/* * Copyright (c) 2013-2023, The PurpleI2P Project * * This file is part of Purple i2pd project and licensed under BSD3 * * See full license text in LICENSE file at top of project tree */ #include <inttypes.h> #include "I2PEndian.h" #include <map> #include <string> #include "Crypto.h" #include "RouterContext.h" #include "I2NPProtocol.h" #include "Tunnel.h" #include "TunnelPool.h" #include "Transports.h" #include "Timestamp.h" #include "Log.h" #include "FS.h" #include "ECIESX25519AEADRatchetSession.h" #include "Garlic.h" namespace i2p { namespace garlic { GarlicRoutingSession::GarlicRoutingSession (GarlicDestination * owner, bool attachLeaseSet): m_Owner (owner), m_LeaseSetUpdateStatus (attachLeaseSet ? eLeaseSetUpdated : eLeaseSetDoNotSend), m_LeaseSetUpdateMsgID (0) { } GarlicRoutingSession::GarlicRoutingSession (): m_Owner (nullptr), m_LeaseSetUpdateStatus (eLeaseSetDoNotSend), m_LeaseSetUpdateMsgID (0) { } GarlicRoutingSession::~GarlicRoutingSession () { } std::shared_ptr<GarlicRoutingPath> GarlicRoutingSession::GetSharedRoutingPath () { if (!m_SharedRoutingPath) return nullptr; uint32_t ts = i2p::util::GetSecondsSinceEpoch (); if (m_SharedRoutingPath->numTimesUsed >= ROUTING_PATH_MAX_NUM_TIMES_USED || !m_SharedRoutingPath->outboundTunnel->IsEstablished () || ts*1000LL > m_SharedRoutingPath->remoteLease->endDate || ts > m_SharedRoutingPath->updateTime + ROUTING_PATH_EXPIRATION_TIMEOUT) m_SharedRoutingPath = nullptr; if (m_SharedRoutingPath) m_SharedRoutingPath->numTimesUsed++; return m_SharedRoutingPath; } void GarlicRoutingSession::SetSharedRoutingPath (std::shared_ptr<GarlicRoutingPath> path) { if (path && path->outboundTunnel && path->remoteLease) { path->updateTime = i2p::util::GetSecondsSinceEpoch (); path->numTimesUsed = 0; } else path = nullptr; m_SharedRoutingPath = path; } bool GarlicRoutingSession::MessageConfirmed (uint32_t msgID) { if (msgID == GetLeaseSetUpdateMsgID ()) { SetLeaseSetUpdateStatus (eLeaseSetUpToDate); SetLeaseSetUpdateMsgID (0); LogPrint (eLogInfo, "Garlic: LeaseSet update confirmed"); return true; } return false; } void GarlicRoutingSession::CleanupUnconfirmedLeaseSet (uint64_t ts) { if (m_LeaseSetUpdateMsgID && ts*1000LL > m_LeaseSetSubmissionTime + LEASET_CONFIRMATION_TIMEOUT) { if (GetOwner ()) GetOwner ()->RemoveDeliveryStatusSession (m_LeaseSetUpdateMsgID); m_LeaseSetUpdateMsgID = 0; } } std::shared_ptr<I2NPMessage> GarlicRoutingSession::CreateEncryptedDeliveryStatusMsg (uint32_t msgID) { auto msg = CreateDeliveryStatusMsg (msgID); if (GetOwner ()) { //encrypt uint8_t key[32], tag[32]; RAND_bytes (key, 32); // random session key RAND_bytes (tag, 32); // random session tag GetOwner ()->SubmitSessionKey (key, tag); ElGamalAESSession garlic (key, tag); msg = garlic.WrapSingleMessage (msg); } return msg; } ElGamalAESSession::ElGamalAESSession (GarlicDestination * owner, std::shared_ptr<const i2p::data::RoutingDestination> destination, int numTags, bool attachLeaseSet): GarlicRoutingSession (owner, attachLeaseSet), m_Destination (destination), m_NumTags (numTags) { // create new session tags and session key RAND_bytes (m_SessionKey, 32); m_Encryption.SetKey (m_SessionKey); } ElGamalAESSession::ElGamalAESSession (const uint8_t * sessionKey, const SessionTag& sessionTag): m_NumTags(1) { memcpy (m_SessionKey, sessionKey, 32); m_Encryption.SetKey (m_SessionKey); m_SessionTags.push_back (sessionTag); m_SessionTags.back ().creationTime = i2p::util::GetSecondsSinceEpoch (); } std::shared_ptr<I2NPMessage> ElGamalAESSession::WrapSingleMessage (std::shared_ptr<const I2NPMessage> msg) { auto m = NewI2NPMessage (); m->Align (12); // in order to get buf aligned to 16 (12 + 4) size_t len = 0; uint8_t * buf = m->GetPayload () + 4; // 4 bytes for length // find non-expired tag bool tagFound = false; SessionTag tag; if (m_NumTags > 0) { uint32_t ts = i2p::util::GetSecondsSinceEpoch (); while (!m_SessionTags.empty ()) { if (ts < m_SessionTags.front ().creationTime + OUTGOING_TAGS_EXPIRATION_TIMEOUT) { tag = m_SessionTags.front (); m_SessionTags.pop_front (); // use same tag only once tagFound = true; break; } else m_SessionTags.pop_front (); // remove expired tag } } // create message if (!tagFound) // new session { LogPrint (eLogInfo, "Garlic: No tags available, will use ElGamal"); if (!m_Destination) { LogPrint (eLogError, "Garlic: Can't use ElGamal for unknown destination"); return nullptr; } // create ElGamal block ElGamalBlock elGamal; memcpy (elGamal.sessionKey, m_SessionKey, 32); RAND_bytes (elGamal.preIV, 32); // Pre-IV uint8_t iv[32]; // IV is first 16 bytes SHA256(elGamal.preIV, 32, iv); m_Destination->Encrypt ((uint8_t *)&elGamal, buf); m_Encryption.SetIV (iv); buf += 514; len += 514; } else // existing session { // session tag memcpy (buf, tag, 32); uint8_t iv[32]; // IV is first 16 bytes SHA256(tag, 32, iv); m_Encryption.SetIV (iv); buf += 32; len += 32; } // AES block len += CreateAESBlock (buf, msg); htobe32buf (m->GetPayload (), len); m->len += len + 4; m->FillI2NPMessageHeader (eI2NPGarlic); return m; } size_t ElGamalAESSession::CreateAESBlock (uint8_t * buf, std::shared_ptr<const I2NPMessage> msg) { size_t blockSize = 0; bool createNewTags = GetOwner () && m_NumTags && ((int)m_SessionTags.size () <= m_NumTags*2/3); UnconfirmedTags * newTags = createNewTags ? GenerateSessionTags () : nullptr; htobuf16 (buf, newTags ? htobe16 (newTags->numTags) : 0); // tag count blockSize += 2; if (newTags) // session tags recreated { for (int i = 0; i < newTags->numTags; i++) { memcpy (buf + blockSize, newTags->sessionTags[i], 32); // tags blockSize += 32; } } uint32_t * payloadSize = (uint32_t *)(buf + blockSize); blockSize += 4; uint8_t * payloadHash = buf + blockSize; blockSize += 32; buf[blockSize] = 0; // flag blockSize++; size_t len = CreateGarlicPayload (buf + blockSize, msg, newTags); htobe32buf (payloadSize, len); SHA256(buf + blockSize, len, payloadHash); blockSize += len; size_t rem = blockSize % 16; if (rem) blockSize += (16-rem); //padding m_Encryption.Encrypt(buf, blockSize, buf); return blockSize; } size_t ElGamalAESSession::CreateGarlicPayload (uint8_t * payload, std::shared_ptr<const I2NPMessage> msg, UnconfirmedTags * newTags) { uint64_t ts = i2p::util::GetMillisecondsSinceEpoch (); uint32_t msgID; RAND_bytes ((uint8_t *)&msgID, 4); size_t size = 0; uint8_t * numCloves = payload + size; *numCloves = 0; size++; if (GetOwner ()) { // resubmit non-confirmed LeaseSet if (GetLeaseSetUpdateStatus () == eLeaseSetSubmitted && ts > GetLeaseSetSubmissionTime () + LEASET_CONFIRMATION_TIMEOUT) { SetLeaseSetUpdateStatus (eLeaseSetUpdated); SetSharedRoutingPath (nullptr); // invalidate path since leaseset was not confirmed } // attach DeviveryStatus if necessary if (newTags || GetLeaseSetUpdateStatus () == eLeaseSetUpdated) // new tags created or leaseset updated { // clove is DeliveryStatus auto cloveSize = CreateDeliveryStatusClove (payload + size, msgID); if (cloveSize > 0) // successive? { size += cloveSize; (*numCloves)++; if (newTags) // new tags created { newTags->msgID = msgID; m_UnconfirmedTagsMsgs.insert (std::make_pair(msgID, std::unique_ptr<UnconfirmedTags>(newTags))); newTags = nullptr; // got acquired } GetOwner ()->DeliveryStatusSent (shared_from_this (), msgID); } else LogPrint (eLogWarning, "Garlic: DeliveryStatus clove was not created"); } // attach LeaseSet if (GetLeaseSetUpdateStatus () == eLeaseSetUpdated) { if (GetLeaseSetUpdateMsgID ()) GetOwner ()->RemoveDeliveryStatusSession (GetLeaseSetUpdateMsgID ()); // remove previous SetLeaseSetUpdateStatus (eLeaseSetSubmitted); SetLeaseSetUpdateMsgID (msgID); SetLeaseSetSubmissionTime (ts); // clove if our leaseSet must be attached auto leaseSet = CreateDatabaseStoreMsg (GetOwner ()->GetLeaseSet ()); size += CreateGarlicClove (payload + size, leaseSet, false); (*numCloves)++; } } if (msg) // clove message itself if presented { size += CreateGarlicClove (payload + size, msg, m_Destination ? m_Destination->IsDestination () : false); (*numCloves)++; } memset (payload + size, 0, 3); // certificate of message size += 3; htobe32buf (payload + size, msgID); // MessageID size += 4; htobe64buf (payload + size, ts + 8000); // Expiration of message, 8 sec size += 8; if (newTags) delete newTags; // not acquired, delete return size; } size_t ElGamalAESSession::CreateGarlicClove (uint8_t * buf, std::shared_ptr<const I2NPMessage> msg, bool isDestination) { uint64_t ts = i2p::util::GetMillisecondsSinceEpoch () + 8000; // 8 sec size_t size = 0; if (isDestination) { buf[size] = eGarlicDeliveryTypeDestination << 5;// delivery instructions flag destination size++; memcpy (buf + size, m_Destination->GetIdentHash (), 32); size += 32; } else { buf[size] = 0;// delivery instructions flag local size++; } memcpy (buf + size, msg->GetBuffer (), msg->GetLength ()); size += msg->GetLength (); uint32_t cloveID; RAND_bytes ((uint8_t *)&cloveID, 4); htobe32buf (buf + size, cloveID); // CloveID size += 4; htobe64buf (buf + size, ts); // Expiration of clove size += 8; memset (buf + size, 0, 3); // certificate of clove size += 3; return size; } size_t ElGamalAESSession::CreateDeliveryStatusClove (uint8_t * buf, uint32_t msgID) { size_t size = 0; if (GetOwner ()) { auto inboundTunnel = GetOwner ()->GetTunnelPool ()->GetNextInboundTunnel (); if (inboundTunnel) { buf[size] = eGarlicDeliveryTypeTunnel << 5; // delivery instructions flag tunnel size++; // hash and tunnelID sequence is reversed for Garlic memcpy (buf + size, inboundTunnel->GetNextIdentHash (), 32); // To Hash size += 32; htobe32buf (buf + size, inboundTunnel->GetNextTunnelID ()); // tunnelID size += 4; // create msg auto msg = CreateEncryptedDeliveryStatusMsg (msgID); if (msg) { memcpy (buf + size, msg->GetBuffer (), msg->GetLength ()); size += msg->GetLength (); } // fill clove uint64_t ts = i2p::util::GetMillisecondsSinceEpoch () + 8000; // 8 sec uint32_t cloveID; RAND_bytes ((uint8_t *)&cloveID, 4); htobe32buf (buf + size, cloveID); // CloveID size += 4; htobe64buf (buf + size, ts); // Expiration of clove size += 8; memset (buf + size, 0, 3); // certificate of clove size += 3; } else LogPrint (eLogError, "Garlic: No inbound tunnels in the pool for DeliveryStatus"); } else LogPrint (eLogWarning, "Garlic: Missing local LeaseSet"); return size; } ElGamalAESSession::UnconfirmedTags * ElGamalAESSession::GenerateSessionTags () { auto tags = new UnconfirmedTags (m_NumTags); tags->tagsCreationTime = i2p::util::GetSecondsSinceEpoch (); for (int i = 0; i < m_NumTags; i++) { RAND_bytes (tags->sessionTags[i], 32); tags->sessionTags[i].creationTime = tags->tagsCreationTime; } return tags; } bool ElGamalAESSession::MessageConfirmed (uint32_t msgID) { TagsConfirmed (msgID); if (!GarlicRoutingSession::MessageConfirmed (msgID)) CleanupExpiredTags (); return true; } void ElGamalAESSession::TagsConfirmed (uint32_t msgID) { uint32_t ts = i2p::util::GetSecondsSinceEpoch (); auto it = m_UnconfirmedTagsMsgs.find (msgID); if (it != m_UnconfirmedTagsMsgs.end ()) { auto& tags = it->second; if (ts < tags->tagsCreationTime + OUTGOING_TAGS_EXPIRATION_TIMEOUT) { for (int i = 0; i < tags->numTags; i++) m_SessionTags.push_back (tags->sessionTags[i]); } m_UnconfirmedTagsMsgs.erase (it); } } bool ElGamalAESSession::CleanupExpiredTags () { auto ts = i2p::util::GetSecondsSinceEpoch (); for (auto it = m_SessionTags.begin (); it != m_SessionTags.end ();) { if (ts >= it->creationTime + OUTGOING_TAGS_EXPIRATION_TIMEOUT) it = m_SessionTags.erase (it); else ++it; } CleanupUnconfirmedTags (); CleanupUnconfirmedLeaseSet (ts); return !m_SessionTags.empty () || !m_UnconfirmedTagsMsgs.empty (); } bool ElGamalAESSession::CleanupUnconfirmedTags () { bool ret = false; uint32_t ts = i2p::util::GetSecondsSinceEpoch (); // delete expired unconfirmed tags for (auto it = m_UnconfirmedTagsMsgs.begin (); it != m_UnconfirmedTagsMsgs.end ();) { if (ts >= it->second->tagsCreationTime + OUTGOING_TAGS_CONFIRMATION_TIMEOUT) { if (GetOwner ()) GetOwner ()->RemoveDeliveryStatusSession (it->first); it = m_UnconfirmedTagsMsgs.erase (it); ret = true; } else ++it; } return ret; } GarlicDestination::GarlicDestination (): m_NumTags (32), // 32 tags by default m_PayloadBuffer (nullptr), m_NumRatchetInboundTags (0) // 0 means standard { } GarlicDestination::~GarlicDestination () { if (m_PayloadBuffer) delete[] m_PayloadBuffer; } void GarlicDestination::CleanUp () { for (auto it: m_Sessions) it.second->SetOwner (nullptr); m_Sessions.clear (); m_DeliveryStatusSessions.clear (); m_Tags.clear (); for (auto it: m_ECIESx25519Sessions) { it.second->Terminate (); it.second->SetOwner (nullptr); } m_ECIESx25519Sessions.clear (); m_ECIESx25519Tags.clear (); } void GarlicDestination::AddSessionKey (const uint8_t * key, const uint8_t * tag) { if (key) { uint32_t ts = i2p::util::GetSecondsSinceEpoch (); m_Tags[SessionTag(tag, ts)] = std::make_shared<AESDecryption>(key); } } void GarlicDestination::AddECIESx25519Key (const uint8_t * key, const uint8_t * tag) { uint64_t t; memcpy (&t, tag, 8); AddECIESx25519Key (key, t); } void GarlicDestination::AddECIESx25519Key (const uint8_t * key, uint64_t tag) { auto tagset = std::make_shared<SymmetricKeyTagSet>(this, key); m_ECIESx25519Tags.emplace (tag, ECIESX25519AEADRatchetIndexTagset{0, tagset}); } bool GarlicDestination::SubmitSessionKey (const uint8_t * key, const uint8_t * tag) { AddSessionKey (key, tag); return true; } void GarlicDestination::SubmitECIESx25519Key (const uint8_t * key, uint64_t tag) { AddECIESx25519Key (key, tag); } void GarlicDestination::HandleGarlicMessage (std::shared_ptr<I2NPMessage> msg) { uint8_t * buf = msg->GetPayload (); uint32_t length = bufbe32toh (buf); if (length > msg->GetLength ()) { LogPrint (eLogWarning, "Garlic: Message length ", length, " exceeds I2NP message length ", msg->GetLength ()); return; } auto mod = length & 0x0f; // %16 buf += 4; // length bool found = false; if (SupportsEncryptionType (i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD)) // try ECIESx25519 tag found = HandleECIESx25519TagMessage (buf, length); if (!found) { auto it = !mod ? m_Tags.find (SessionTag(buf)) : m_Tags.end (); // AES block is multiple of 16 // AES tag might be used even if encryption type is not ElGamal/AES if (it != m_Tags.end ()) // try AES tag { // tag found. Use AES auto decryption = it->second; m_Tags.erase (it); // tag might be used only once if (length >= 32) { uint8_t iv[32]; // IV is first 16 bytes SHA256(buf, 32, iv); decryption->SetIV (iv); decryption->Decrypt (buf + 32, length - 32, buf + 32); HandleAESBlock (buf + 32, length - 32, decryption, msg->from); found = true; } else LogPrint (eLogWarning, "Garlic: Message length ", length, " is less than 32 bytes"); } if (!found) // assume new session { // AES tag not found. Handle depending on encryption type // try ElGamal/AES first if leading block is 514 ElGamalBlock elGamal; if (mod == 2 && length >= 514 && SupportsEncryptionType (i2p::data::CRYPTO_KEY_TYPE_ELGAMAL) && Decrypt (buf, (uint8_t *)&elGamal, i2p::data::CRYPTO_KEY_TYPE_ELGAMAL)) { auto decryption = std::make_shared<AESDecryption>(elGamal.sessionKey); uint8_t iv[32]; // IV is first 16 bytes SHA256(elGamal.preIV, 32, iv); decryption->SetIV (iv); decryption->Decrypt(buf + 514, length - 514, buf + 514); HandleAESBlock (buf + 514, length - 514, decryption, msg->from); } else if (SupportsEncryptionType (i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD)) { // otherwise ECIESx25519 auto session = std::make_shared<ECIESX25519AEADRatchetSession> (this, false); // incoming if (!session->HandleNextMessage (buf, length, nullptr, 0)) { // try to generate more tags for last tagset if (m_LastTagset && (m_LastTagset->GetNextIndex () - m_LastTagset->GetTrimBehind () < 3*ECIESX25519_MAX_NUM_GENERATED_TAGS)) { uint64_t missingTag; memcpy (&missingTag, buf, 8); auto maxTags = std::max (m_NumRatchetInboundTags, ECIESX25519_MAX_NUM_GENERATED_TAGS); LogPrint (eLogWarning, "Garlic: Trying to generate more ECIES-X25519-AEAD-Ratchet tags"); for (int i = 0; i < maxTags; i++) { auto nextTag = AddECIESx25519SessionNextTag (m_LastTagset); if (!nextTag) { LogPrint (eLogError, "Garlic: Can't create new ECIES-X25519-AEAD-Ratchet tag for last tagset"); break; } if (nextTag == missingTag) { LogPrint (eLogDebug, "Garlic: Missing ECIES-X25519-AEAD-Ratchet tag was generated"); if (m_LastTagset->HandleNextMessage (buf, length, m_ECIESx25519Tags[nextTag].index)) found = true; break; } } if (!found) m_LastTagset = nullptr; } if (!found) LogPrint (eLogError, "Garlic: Can't handle ECIES-X25519-AEAD-Ratchet message"); } } else LogPrint (eLogError, "Garlic: Failed to decrypt message"); } } } bool GarlicDestination::HandleECIESx25519TagMessage (uint8_t * buf, size_t len) { uint64_t tag; memcpy (&tag, buf, 8); auto it = m_ECIESx25519Tags.find (tag); if (it != m_ECIESx25519Tags.end ()) { if (it->second.tagset && it->second.tagset->HandleNextMessage (buf, len, it->second.index)) m_LastTagset = it->second.tagset; else LogPrint (eLogError, "Garlic: Can't handle ECIES-X25519-AEAD-Ratchet message"); m_ECIESx25519Tags.erase (it); return true; } return false; } void GarlicDestination::HandleAESBlock (uint8_t * buf, size_t len, std::shared_ptr<AESDecryption> decryption, std::shared_ptr<i2p::tunnel::InboundTunnel> from) { uint16_t tagCount = bufbe16toh (buf); buf += 2; len -= 2; if (tagCount > 0) { if (tagCount*32 > len) { LogPrint (eLogError, "Garlic: Tag count ", tagCount, " exceeds length ", len); return ; } uint32_t ts = i2p::util::GetSecondsSinceEpoch (); for (int i = 0; i < tagCount; i++) m_Tags[SessionTag(buf + i*32, ts)] = decryption; } buf += tagCount*32; len -= tagCount*32; uint32_t payloadSize = bufbe32toh (buf); if (payloadSize > len) { LogPrint (eLogError, "Garlic: Unexpected payload size ", payloadSize); return; } buf += 4; uint8_t * payloadHash = buf; buf += 32;// payload hash. if (*buf) // session key? buf += 32; // new session key buf++; // flag // payload uint8_t digest[32]; SHA256 (buf, payloadSize, digest); if (memcmp (payloadHash, digest, 32)) // payload hash doesn't match { LogPrint (eLogError, "Garlic: Wrong payload hash"); return; } HandleGarlicPayload (buf, payloadSize, from); } void GarlicDestination::HandleGarlicPayload (uint8_t * buf, size_t len, std::shared_ptr<i2p::tunnel::InboundTunnel> from) { if (len < 1) { LogPrint (eLogError, "Garlic: Payload is too short"); return; } int numCloves = buf[0]; LogPrint (eLogDebug, "Garlic: ", numCloves," cloves"); buf++; len--; for (int i = 0; i < numCloves; i++) { const uint8_t * buf1 = buf; // delivery instructions uint8_t flag = buf[0]; buf++; // flag if (flag & 0x80) // encrypted? { // TODO: implement LogPrint (eLogWarning, "Garlic: Clove encrypted"); buf += 32; } ptrdiff_t offset = buf - buf1; GarlicDeliveryType deliveryType = (GarlicDeliveryType)((flag >> 5) & 0x03); switch (deliveryType) { case eGarlicDeliveryTypeLocal: LogPrint (eLogDebug, "Garlic: Type local"); if (offset > (int)len) { LogPrint (eLogError, "Garlic: Message is too short"); break; } HandleI2NPMessage (buf, len - offset); break; case eGarlicDeliveryTypeDestination: LogPrint (eLogDebug, "Garlic: Type destination"); buf += 32; // destination. check it later or for multiple destinations offset = buf - buf1; if (offset > (int)len) { LogPrint (eLogError, "Garlic: Message is too short"); break; } HandleI2NPMessage (buf, len - offset); break; case eGarlicDeliveryTypeTunnel: { LogPrint (eLogDebug, "Garlic: Type tunnel"); // gwHash and gwTunnel sequence is reverted uint8_t * gwHash = buf; buf += 32; offset = buf - buf1; if (offset + 4 > (int)len) { LogPrint (eLogError, "Garlic: Message is too short"); break; } uint32_t gwTunnel = bufbe32toh (buf); buf += 4; offset += 4; auto msg = CreateI2NPMessage (buf, GetI2NPMessageLength (buf, len - offset), from); if (from) // received through an inbound tunnel { std::shared_ptr<i2p::tunnel::OutboundTunnel> tunnel; if (from->GetTunnelPool ()) tunnel = from->GetTunnelPool ()->GetNextOutboundTunnel (); else LogPrint (eLogError, "Garlic: Tunnel pool is not set for inbound tunnel"); if (tunnel) // we have sent it through an outbound tunnel tunnel->SendTunnelDataMsgTo (gwHash, gwTunnel, msg); else LogPrint (eLogWarning, "Garlic: No outbound tunnels available for garlic clove"); } else // received directly i2p::transport::transports.SendMessage (gwHash, i2p::CreateTunnelGatewayMsg (gwTunnel, msg)); // send directly break; } case eGarlicDeliveryTypeRouter: { uint8_t * ident = buf; buf += 32; offset = buf - buf1; if (!from) // received directly { if (offset > (int)len) { LogPrint (eLogError, "Garlic: Message is too short"); break; } i2p::transport::transports.SendMessage (ident, CreateI2NPMessage (buf, GetI2NPMessageLength (buf, len - offset))); } else LogPrint (eLogWarning, "Garlic: Type router for inbound tunnels not supported"); break; } default: LogPrint (eLogWarning, "Garlic: Unknown delivery type ", (int)deliveryType); } if (offset > (int)len) { LogPrint (eLogError, "Garlic: Message is too short"); break; } buf += GetI2NPMessageLength (buf, len - offset); // I2NP buf += 4; // CloveID buf += 8; // Date buf += 3; // Certificate offset = buf - buf1; if (offset > (int)len) { LogPrint (eLogError, "Garlic: Clove is too long"); break; } len -= offset; } } std::shared_ptr<I2NPMessage> GarlicDestination::WrapMessageForRouter (std::shared_ptr<const i2p::data::RouterInfo> router, std::shared_ptr<I2NPMessage> msg) { if (router->GetEncryptionType () == i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD) return WrapECIESX25519MessageForRouter (msg, router->GetIdentity ()->GetEncryptionPublicKey ()); else { auto session = GetRoutingSession (router, false); return session->WrapSingleMessage (msg); } } std::shared_ptr<GarlicRoutingSession> GarlicDestination::GetRoutingSession ( std::shared_ptr<const i2p::data::RoutingDestination> destination, bool attachLeaseSet) { if (destination->GetEncryptionType () == i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD && SupportsEncryptionType (i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD)) { ECIESX25519AEADRatchetSessionPtr session; uint8_t staticKey[32]; destination->Encrypt (nullptr, staticKey); // we are supposed to get static key auto it = m_ECIESx25519Sessions.find (staticKey); if (it != m_ECIESx25519Sessions.end ()) { session = it->second; if (session->IsInactive (i2p::util::GetSecondsSinceEpoch ())) { LogPrint (eLogDebug, "Garlic: Session restarted"); session = nullptr; } } if (!session) { session = std::make_shared<ECIESX25519AEADRatchetSession> (this, true); session->SetRemoteStaticKey (staticKey); } if (destination->IsDestination ()) session->SetDestination (destination->GetIdentHash ()); // TODO: remove return session; } else { ElGamalAESSessionPtr session; { std::unique_lock<std::mutex> l(m_SessionsMutex); auto it = m_Sessions.find (destination->GetIdentHash ()); if (it != m_Sessions.end ()) session = it->second; } if (!session) { session = std::make_shared<ElGamalAESSession> (this, destination, attachLeaseSet ? m_NumTags : 4, attachLeaseSet); // specified num tags for connections and 4 for LS requests std::unique_lock<std::mutex> l(m_SessionsMutex); m_Sessions[destination->GetIdentHash ()] = session; } return session; } return nullptr; } void GarlicDestination::CleanupExpiredTags () { // incoming uint32_t ts = i2p::util::GetSecondsSinceEpoch (); int numExpiredTags = 0; for (auto it = m_Tags.begin (); it != m_Tags.end ();) { if (ts > it->first.creationTime + INCOMING_TAGS_EXPIRATION_TIMEOUT) { numExpiredTags++; it = m_Tags.erase (it); } else ++it; } if (numExpiredTags > 0) LogPrint (eLogDebug, "Garlic: ", numExpiredTags, " tags expired for ", GetIdentHash().ToBase64 ()); // outgoing { std::unique_lock<std::mutex> l(m_SessionsMutex); for (auto it = m_Sessions.begin (); it != m_Sessions.end ();) { it->second->GetSharedRoutingPath (); // delete shared path if necessary if (!it->second->CleanupExpiredTags ()) { LogPrint (eLogInfo, "Garlic: Routing session to ", it->first.ToBase32 (), " deleted"); it->second->SetOwner (nullptr); it = m_Sessions.erase (it); } else ++it; } } // delivery status sessions { std::unique_lock<std::mutex> l(m_DeliveryStatusSessionsMutex); for (auto it = m_DeliveryStatusSessions.begin (); it != m_DeliveryStatusSessions.end (); ) { if (it->second->GetOwner () != this) it = m_DeliveryStatusSessions.erase (it); else ++it; } } // ECIESx25519 for (auto it = m_ECIESx25519Sessions.begin (); it != m_ECIESx25519Sessions.end ();) { if (it->second->CheckExpired (ts)) { it->second->Terminate (); it = m_ECIESx25519Sessions.erase (it); } else ++it; } numExpiredTags = 0; for (auto it = m_ECIESx25519Tags.begin (); it != m_ECIESx25519Tags.end ();) { if (it->second.tagset->IsExpired (ts) || it->second.tagset->IsIndexExpired (it->second.index)) { it->second.tagset->DeleteSymmKey (it->second.index); it = m_ECIESx25519Tags.erase (it); numExpiredTags++; } else { auto session = it->second.tagset->GetSession (); if (!session || session->IsTerminated()) { it = m_ECIESx25519Tags.erase (it); numExpiredTags++; } else ++it; } } if (numExpiredTags > 0) LogPrint (eLogDebug, "Garlic: ", numExpiredTags, " ECIESx25519 tags expired for ", GetIdentHash().ToBase64 ()); if (m_LastTagset && m_LastTagset->IsExpired (ts)) m_LastTagset = nullptr; } void GarlicDestination::RemoveDeliveryStatusSession (uint32_t msgID) { std::unique_lock<std::mutex> l(m_DeliveryStatusSessionsMutex); m_DeliveryStatusSessions.erase (msgID); } void GarlicDestination::DeliveryStatusSent (GarlicRoutingSessionPtr session, uint32_t msgID) { std::unique_lock<std::mutex> l(m_DeliveryStatusSessionsMutex); m_DeliveryStatusSessions[msgID] = session; } void GarlicDestination::HandleDeliveryStatusMessage (uint32_t msgID) { GarlicRoutingSessionPtr session; { std::unique_lock<std::mutex> l(m_DeliveryStatusSessionsMutex); auto it = m_DeliveryStatusSessions.find (msgID); if (it != m_DeliveryStatusSessions.end ()) { session = it->second; m_DeliveryStatusSessions.erase (it); } } if (session) { session->MessageConfirmed (msgID); LogPrint (eLogDebug, "Garlic: Message ", msgID, " acknowledged"); } } void GarlicDestination::SetLeaseSetUpdated () { { std::unique_lock<std::mutex> l(m_SessionsMutex); for (auto& it: m_Sessions) it.second->SetLeaseSetUpdated (); } for (auto& it: m_ECIESx25519Sessions) it.second->SetLeaseSetUpdated (); } void GarlicDestination::ProcessGarlicMessage (std::shared_ptr<I2NPMessage> msg) { HandleGarlicMessage (msg); } void GarlicDestination::ProcessDeliveryStatusMessage (std::shared_ptr<I2NPMessage> msg) { uint32_t msgID = bufbe32toh (msg->GetPayload () + DELIVERY_STATUS_MSGID_OFFSET); HandleDeliveryStatusMessage (msgID); } void GarlicDestination::SaveTags () { if (m_Tags.empty ()) return; std::string ident = GetIdentHash().ToBase32(); std::string path = i2p::fs::DataDirPath("tags", (ident + ".tags")); std::ofstream f (path, std::ofstream::binary | std::ofstream::out | std::ofstream::trunc); uint32_t ts = i2p::util::GetSecondsSinceEpoch (); // 4 bytes timestamp, 32 bytes tag, 32 bytes key for (auto it: m_Tags) { if (ts < it.first.creationTime + INCOMING_TAGS_EXPIRATION_TIMEOUT) { f.write ((char *)&it.first.creationTime, 4); f.write ((char *)it.first.data (), 32); f.write ((char *)it.second->GetKey ().data (), 32); } } } void GarlicDestination::LoadTags () { std::string ident = GetIdentHash().ToBase32(); std::string path = i2p::fs::DataDirPath("tags", (ident + ".tags")); uint32_t ts = i2p::util::GetSecondsSinceEpoch (); if (ts < i2p::fs::GetLastUpdateTime (path) + INCOMING_TAGS_EXPIRATION_TIMEOUT) { // might contain non-expired tags std::ifstream f (path, std::ifstream::binary); if (f) { std::map<i2p::crypto::AESKey, std::shared_ptr<AESDecryption> > keys; // 4 bytes timestamp, 32 bytes tag, 32 bytes key while (!f.eof ()) { uint32_t t; uint8_t tag[32], key[32]; f.read ((char *)&t, 4); if (f.eof ()) break; if (ts < t + INCOMING_TAGS_EXPIRATION_TIMEOUT) { f.read ((char *)tag, 32); f.read ((char *)key, 32); } else f.seekg (64, std::ios::cur); // skip if (f.eof ()) break; std::shared_ptr<AESDecryption> decryption; auto it = keys.find (key); if (it != keys.end ()) decryption = it->second; else decryption = std::make_shared<AESDecryption>(key); m_Tags.insert (std::make_pair (SessionTag (tag, ts), decryption)); } if (!m_Tags.empty ()) LogPrint (eLogInfo, "Garlic: ", m_Tags.size (), " tags loaded for ", ident); } } i2p::fs::Remove (path); } void CleanUpTagsFiles () { std::vector<std::string> files; i2p::fs::ReadDir (i2p::fs::DataDirPath("tags"), files); uint32_t ts = i2p::util::GetSecondsSinceEpoch (); for (auto it: files) if (ts >= i2p::fs::GetLastUpdateTime (it) + INCOMING_TAGS_EXPIRATION_TIMEOUT) i2p::fs::Remove (it); } void GarlicDestination::HandleECIESx25519GarlicClove (const uint8_t * buf, size_t len) { const uint8_t * buf1 = buf; uint8_t flag = buf[0]; buf++; // flag GarlicDeliveryType deliveryType = (GarlicDeliveryType)((flag >> 5) & 0x03); switch (deliveryType) { case eGarlicDeliveryTypeDestination: LogPrint (eLogDebug, "Garlic: Type destination"); buf += 32; // TODO: check destination #if (__cplusplus >= 201703L) // C++ 17 or higher [[fallthrough]]; #endif // no break here case eGarlicDeliveryTypeLocal: { LogPrint (eLogDebug, "Garlic: Type local"); I2NPMessageType typeID = (I2NPMessageType)(buf[0]); buf++; // typeid int32_t msgID = bufbe32toh (buf); buf += 4; // msgID buf += 4; // expiration ptrdiff_t offset = buf - buf1; if (offset <= (int)len) HandleCloveI2NPMessage (typeID, buf, len - offset, msgID); else LogPrint (eLogError, "Garlic: Clove is too long"); break; } case eGarlicDeliveryTypeTunnel: { LogPrint (eLogDebug, "Garlic: Type tunnel"); // gwHash and gwTunnel sequence is reverted const uint8_t * gwHash = buf; buf += 32; ptrdiff_t offset = buf - buf1; if (offset + 13 > (int)len) { LogPrint (eLogError, "Garlic: Message is too short"); break; } uint32_t gwTunnel = bufbe32toh (buf); buf += 4; I2NPMessageType typeID = (I2NPMessageType)(buf[0]); buf++; // typeid uint32_t msgID = bufbe32toh (buf); buf += 4; // msgID buf += 4; // expiration offset += 13; if (GetTunnelPool ()) { auto tunnel = GetTunnelPool ()->GetNextOutboundTunnel (); if (tunnel) tunnel->SendTunnelDataMsgTo (gwHash, gwTunnel, CreateI2NPMessage (typeID, buf, len - offset, msgID)); else LogPrint (eLogWarning, "Garlic: No outbound tunnels available for garlic clove"); } else LogPrint (eLogError, "Garlic: Tunnel pool is not set for inbound tunnel"); break; } default: LogPrint (eLogWarning, "Garlic: Unexpected delivery type ", (int)deliveryType); } } uint64_t GarlicDestination::AddECIESx25519SessionNextTag (ReceiveRatchetTagSetPtr tagset) { auto index = tagset->GetNextIndex (); uint64_t tag = tagset->GetNextSessionTag (); if (tag) m_ECIESx25519Tags.emplace (tag, ECIESX25519AEADRatchetIndexTagset{index, tagset}); return tag; } void GarlicDestination::AddECIESx25519Session (const uint8_t * staticKey, ECIESX25519AEADRatchetSessionPtr session) { i2p::data::Tag<32> staticKeyTag (staticKey); auto it = m_ECIESx25519Sessions.find (staticKeyTag); if (it != m_ECIESx25519Sessions.end ()) { if (it->second->CanBeRestarted (i2p::util::GetSecondsSinceEpoch ())) { it->second->Terminate (); // detach m_ECIESx25519Sessions.erase (it); } else { LogPrint (eLogInfo, "Garlic: ECIESx25519 session with static key ", staticKeyTag.ToBase64 (), " already exists"); return; } } m_ECIESx25519Sessions.emplace (staticKeyTag, session); } void GarlicDestination::RemoveECIESx25519Session (const uint8_t * staticKey) { auto it = m_ECIESx25519Sessions.find (staticKey); if (it != m_ECIESx25519Sessions.end ()) { it->second->Terminate (); m_ECIESx25519Sessions.erase (it); } } uint8_t * GarlicDestination::GetPayloadBuffer () { if (!m_PayloadBuffer) m_PayloadBuffer = new uint8_t[I2NP_MAX_MESSAGE_SIZE]; return m_PayloadBuffer; } } }