diff --git a/lib/transport/ntcp/handshake.go b/lib/transport/ntcp/handshake.go index 5369434..a0f0c11 100644 --- a/lib/transport/ntcp/handshake.go +++ b/lib/transport/ntcp/handshake.go @@ -8,60 +8,8 @@ import ( "github.com/samber/oops" ) -// sendSessionRequest sends Message 1 (SessionRequest) to the remote peer -func (c *NTCP2Session) sendSessionRequest(conn net.Conn, hs *handshake.HandshakeState) error { - - log.Debugf("NTCP2: Sending SessionRequest message") - // 1. Create and send X (ephemeral key) | Padding - // uses CreateSessionRequest from session_request.go - sessionRequestMessage, err := c.CreateSessionRequest() - if err != nil { - return oops.Errorf("failed to create session request: %v", err) - } - // 2. Set deadline for the connection - if err := conn.SetDeadline(time.Now().Add(NTCP2_HANDSHAKE_TIMEOUT)); err != nil { - return oops.Errorf("failed to set deadline: %v", err) - } - // 3. Obfuscate the session request message - obfuscatedX, err := c.ObfuscateEphemeral(sessionRequestMessage.XContent[:]) - if err != nil { - return oops.Errorf("failed to obfuscate ephemeral key: %v", err) - } - // 4. ChaChaPoly Frame - // Encrypt options block and authenticate both options and padding - ciphertext, err := c.encryptSessionRequestOptions(sessionRequestMessage, obfuscatedX) - if err != nil { - return err - } - - // Combine all components into final message - // 1. Obfuscated X (already in obfuscatedX) - // 2. ChaCha20-Poly1305 encrypted options with auth tag - // 3. Authenticated but unencrypted padding - message := append(obfuscatedX, ciphertext...) - message = append(message, sessionRequestMessage.Padding...) - - // 5. Write the message to the connection - if _, err := conn.Write(message); err != nil { - return oops.Errorf("failed to send session request: %v", err) - } - return nil -} - // receiveSessionRequest processes Message 1 (SessionRequest) from remote func (c *NTCP2Session) receiveSessionRequest(conn net.Conn, hs *handshake.HandshakeState) error { - /* - receiveSessionRequest processes incoming NTCP2 Message 1 (SessionRequest): - 1. Read and buffer the fixed-length ephemeral key portion (X) - 2. Deobfuscate X using AES with local router hash as key - 3. Validate the ephemeral key (X) is a valid Curve25519 point - 4. Read the ChaCha20-Poly1305 encrypted options block - 5. Derive KDF for handshake message 1 using X and local static key - 6. Decrypt and authenticate the options block - 7. Extract and validate handshake parameters (timestamp, version, padding length) - 8. Read and validate any padding bytes - 9. Check timestamp for acceptable clock skew (±60 seconds?) - */ log.Debugf("NTCP2: Processing incoming SessionRequest message") // Read the ephemeral key (X) @@ -101,18 +49,6 @@ func (c *NTCP2Session) receiveSessionRequest(conn net.Conn, hs *handshake.Handsh // sendSessionCreated sends Message 2 (SessionCreated) to the remote peer func (c *NTCP2Session) sendSessionCreated(conn net.Conn, hs *handshake.HandshakeState) error { - /* - sendSessionCreated implements NTCP2 Message 2 (SessionCreated): - 1. Generate ephemeral Y keypair for responder side - 2. Calculate current timestamp for clock skew verification - 3. Create options block (timestamp, padding length, etc.) - 4. Obfuscate Y using AES with same key as message 1 - 5. Derive KDF for handshake message 2 using established state - 6. Encrypt options block using ChaCha20-Poly1305 - 7. Generate random padding according to negotiated parameters - 8. Assemble final message: obfuscated Y + encrypted options + padding - 9. Write complete message to connection - */ // Implement according to NTCP2 spec // uses CreateSessionCreated from session_created.go // see also: session_created.go, messages/session_created.go @@ -157,20 +93,6 @@ func (c *NTCP2Session) sendSessionCreated(conn net.Conn, hs *handshake.Handshake // receiveSessionCreated processes Message 2 (SessionCreated) from remote func (c *NTCP2Session) receiveSessionCreated(conn net.Conn, hs *handshake.HandshakeState) error { - /* - receiveSessionCreated processes incoming NTCP2 Message 2 (SessionCreated): - 1. Read and buffer the fixed-length ephemeral key portion (Y) - 2. Deobfuscate Y using AES with same state as message 1 - 3. Validate the ephemeral key (Y) is a valid Curve25519 point - 4. Read the ChaCha20-Poly1305 encrypted options block - 5. Derive KDF for handshake message 2 using established state and Y - 6. Decrypt and authenticate the options block - 7. Extract and validate handshake parameters (timestamp, padding length) - 8. Read and validate any padding bytes - 9. Compute DH with local ephemeral and remote ephemeral (ee) - 10. Check timestamp for acceptable clock skew (±60 seconds?) - 11. Adjust local state with received parameters - */ // Implement according to NTCP2 spec // uses CreateSessionCreated from session_created.go // see also: session_created.go, messages/session_created.go @@ -180,22 +102,6 @@ func (c *NTCP2Session) receiveSessionCreated(conn net.Conn, hs *handshake.Handsh // sendSessionConfirm sends Message 3 (SessionConfirm) to the remote peer func (c *NTCP2Session) sendSessionConfirm(conn net.Conn, hs *handshake.HandshakeState) error { - /* - sendSessionConfirm implements NTCP2 Message 3 (SessionConfirmed): - 1. Create two separate ChaChaPoly frames for this message - 2. For first frame: - a. Extract local static key (s) - b. Derive KDF for handshake message 3 part 1 - c. Encrypt static key using ChaCha20-Poly1305 - 3. For second frame: - a. Prepare payload with local RouterInfo, options, and padding - b. Derive KDF for handshake message 3 part 2 using se pattern - c. Encrypt payload using ChaCha20-Poly1305 - 4. Assemble final message: encrypted static key frame + encrypted payload frame - 5. Write complete message to connection - 6. Derive final data phase keys (k_ab, k_ba) using Split() operation - 7. Initialize SipHash keys for data phase length obfuscation - */ // Implement according to NTCP2 spec // uses CreateSessionConfirmed from session_confirm.go // see also: session_confirmed.go, messages/session_confirmed.go @@ -205,22 +111,6 @@ func (c *NTCP2Session) sendSessionConfirm(conn net.Conn, hs *handshake.Handshake // receiveSessionConfirm processes Message 3 (SessionConfirm) from remote func (c *NTCP2Session) receiveSessionConfirm(conn net.Conn, hs *handshake.HandshakeState) error { - /* - receiveSessionConfirm processes incoming NTCP2 Message 3 (SessionConfirmed): - 1. Read first ChaChaPoly frame containing encrypted static key - 2. Derive KDF for handshake message 3 part 1 - 3. Decrypt and authenticate static key frame - 4. Validate decrypted static key is a valid Curve25519 point - 5. Read second ChaChaPoly frame with size specified in message 1 - 6. Derive KDF for handshake message 3 part 2 using se pattern - 7. Decrypt and authenticate second frame - 8. Extract RouterInfo from decrypted payload - 9. Validate RouterInfo matches expected router identity - 10. Process any options included in the payload - 11. Derive final data phase keys (k_ab, k_ba) using Split() operation - 12. Initialize SipHash keys for data phase length obfuscation - 13. Mark handshake as complete - */ // Implement according to NTCP2 spec // uses CreateSessionConfirmed from session_confirm.go // see also: session_confirmed.go, messages/session_confirmed.go diff --git a/lib/transport/ntcp/message.go b/lib/transport/ntcp/message.go new file mode 100644 index 0000000..61185d7 --- /dev/null +++ b/lib/transport/ntcp/message.go @@ -0,0 +1,70 @@ +package ntcp + +import ( + "net" + "time" + + "github.com/go-i2p/go-i2p/lib/transport/ntcp/handshake" + "github.com/samber/oops" +) + +func (c *NTCP2Session) sendHandshakeMessage(conn net.Conn, hs *handshake.HandshakeState, processor handshake.HandshakeMessageProcessor) error { + // 1. Create message + message, err := processor.CreateMessage(hs) + if err != nil { + return oops.Errorf("failed to create message: %w", err) + } + + // 2. Set deadline + if err := conn.SetDeadline(time.Now().Add(NTCP2_HANDSHAKE_TIMEOUT)); err != nil { + return oops.Errorf("failed to set deadline: %w", err) + } + + // 3. Obfuscate key + obfuscatedKey, err := processor.ObfuscateKey(message, hs) + if err != nil { + return oops.Errorf("failed to obfuscate key: %w", err) + } + + // 4. Encrypt options + ciphertext, err := processor.EncryptPayload(message, obfuscatedKey, hs) + if err != nil { + return oops.Errorf("failed to encrypt options: %w", err) + } + + // 5. Assemble message + fullMessage := append(obfuscatedKey, ciphertext...) + fullMessage = append(fullMessage, processor.GetPadding(message)...) + + // 6. Write message + if _, err := conn.Write(fullMessage); err != nil { + return oops.Errorf("failed to send message: %w", err) + } + + return nil +} + +// receiveAndProcessHandshakeMessage receives and processes a handshake message using the specified processor +func (s *NTCP2Session) receiveAndProcessHandshakeMessage( + conn net.Conn, + hs *handshake.HandshakeState, + processor handshake.HandshakeMessageProcessor, +) error { + // 1. Set deadline + if err := conn.SetDeadline(time.Now().Add(NTCP2_HANDSHAKE_TIMEOUT)); err != nil { + return oops.Errorf("failed to set deadline: %w", err) + } + + // 2. Read the message + message, err := processor.ReadMessage(conn, hs) + if err != nil { + return oops.Errorf("failed to read message: %w", err) + } + + // 3. Process the message + if err := processor.ProcessMessage(message, hs); err != nil { + return oops.Errorf("failed to process message: %w", err) + } + + return nil +} diff --git a/lib/transport/ntcp/session.go b/lib/transport/ntcp/session.go index 9206dcd..ed20d1f 100644 --- a/lib/transport/ntcp/session.go +++ b/lib/transport/ntcp/session.go @@ -4,14 +4,12 @@ import ( "crypto" "crypto/hmac" - "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/curve25519" "github.com/go-i2p/go-i2p/lib/common/router_info" "github.com/go-i2p/go-i2p/lib/crypto/aes" "github.com/go-i2p/go-i2p/lib/transport/noise" "github.com/go-i2p/go-i2p/lib/transport/ntcp/handshake" - "github.com/go-i2p/go-i2p/lib/transport/ntcp/messages" "github.com/go-i2p/go-i2p/lib/transport/obfs" "github.com/go-i2p/go-i2p/lib/transport/padding" "github.com/go-i2p/go-i2p/lib/util/time/sntp" @@ -173,32 +171,6 @@ func (c *NTCP2Session) computeSharedSecret(ephemeralKey, param []byte) ([]byte, return sharedSecret[:], nil } -func (c *NTCP2Session) encryptSessionRequestOptions(sessionRequestMessage *messages.SessionRequest, obfuscatedX []byte) ([]byte, error) { - chacha20Key, err := c.deriveChacha20Key(sessionRequestMessage.XContent[:]) - if err != nil { - return nil, oops.Errorf("failed to derive ChaCha20 key: %v", err) - } - - // Create AEAD cipher - aead, err := chacha20poly1305.New(chacha20Key) - if err != nil { - return nil, oops.Errorf("failed to create ChaCha20-Poly1305 cipher: %v", err) - } - - // Prepare the nonce (all zeros for first message) - nonce := make([]byte, chacha20poly1305.NonceSize) - - // Create associated data (AD) according to NTCP2 spec: - // AD = obfuscated X value (ensures binding between the AES and ChaCha layers) - ad := obfuscatedX - - // Encrypt options block and authenticate both options and padding - // ChaCha20-Poly1305 encrypts plaintext and appends auth tag - optionsData := sessionRequestMessage.Options.Data() - ciphertext := aead.Seal(nil, nonce, optionsData, ad) - return ciphertext, nil -} - // deriveSessionKeys computes the session keys from the completed handshake func (c *NTCP2Session) deriveSessionKeys(hs *handshake.HandshakeState) error { // Use shared secrets to derive session keys diff --git a/lib/transport/ntcp/session_confirmed_new.go b/lib/transport/ntcp/session_confirmed_new.go index 56cc50c..bf57091 100644 --- a/lib/transport/ntcp/session_confirmed_new.go +++ b/lib/transport/ntcp/session_confirmed_new.go @@ -7,6 +7,38 @@ import ( "github.com/go-i2p/go-i2p/lib/transport/ntcp/messages" ) +/* +SessionConfirmedProcessor implements NTCP2 Message 3 (SessionConfirmed): +1. Create two separate ChaChaPoly frames for this message +2. For first frame: + a. Extract local static key (s) + b. Derive KDF for handshake message 3 part 1 + c. Encrypt static key using ChaCha20-Poly1305 +3. For second frame: + a. Prepare payload with local RouterInfo, options, and padding + b. Derive KDF for handshake message 3 part 2 using se pattern + c. Encrypt payload using ChaCha20-Poly1305 +4. Assemble final message: encrypted static key frame + encrypted payload frame +5. Write complete message to connection +6. Derive final data phase keys (k_ab, k_ba) using Split() operation +7. Initialize SipHash keys for data phase length obfuscation + +SessionConfirmedProcessor processes incoming NTCP2 Message 3 (SessionConfirmed): +1. Read first ChaChaPoly frame containing encrypted static key +2. Derive KDF for handshake message 3 part 1 +3. Decrypt and authenticate static key frame +4. Validate decrypted static key is a valid Curve25519 point +5. Read second ChaChaPoly frame with size specified in message 1 +6. Derive KDF for handshake message 3 part 2 using se pattern +7. Decrypt and authenticate second frame +8. Extract RouterInfo from decrypted payload +9. Validate RouterInfo matches expected router identity +10. Process any options included in the payload +11. Derive final data phase keys (k_ab, k_ba) using Split() operation +12. Initialize SipHash keys for data phase length obfuscation +13. Mark handshake as complete +*/ + type SessionConfirmedProcessor struct { *NTCP2Session } diff --git a/lib/transport/ntcp/session_created_new.go b/lib/transport/ntcp/session_created_new.go index 0d4a765..0e747d1 100644 --- a/lib/transport/ntcp/session_created_new.go +++ b/lib/transport/ntcp/session_created_new.go @@ -7,6 +7,32 @@ import ( "github.com/go-i2p/go-i2p/lib/transport/ntcp/messages" ) +/* +SessionCreatedProcessor implements NTCP2 Message 2 (SessionCreated): +1. Generate ephemeral Y keypair for responder side +2. Calculate current timestamp for clock skew verification +3. Create options block (timestamp, padding length, etc.) +4. Obfuscate Y using AES with same key as message 1 +5. Derive KDF for handshake message 2 using established state +6. Encrypt options block using ChaCha20-Poly1305 +7. Generate random padding according to negotiated parameters +8. Assemble final message: obfuscated Y + encrypted options + padding +9. Write complete message to connection + +SessionCreatedProcessor processes incoming NTCP2 Message 2 (SessionCreated): +1. Read and buffer the fixed-length ephemeral key portion (Y) +2. Deobfuscate Y using AES with same state as message 1 +3. Validate the ephemeral key (Y) is a valid Curve25519 point +4. Read the ChaCha20-Poly1305 encrypted options block +5. Derive KDF for handshake message 2 using established state and Y +6. Decrypt and authenticate the options block +7. Extract and validate handshake parameters (timestamp, padding length) +8. Read and validate any padding bytes +9. Compute DH with local ephemeral and remote ephemeral (ee) +10. Check timestamp for acceptable clock skew (±60 seconds?) +11. Adjust local state with received parameters +*/ + type SessionCreatedProcessor struct { *NTCP2Session } diff --git a/lib/transport/ntcp/session_request.go b/lib/transport/ntcp/session_request.go index e474246..038cf9c 100644 --- a/lib/transport/ntcp/session_request.go +++ b/lib/transport/ntcp/session_request.go @@ -11,8 +11,10 @@ import ( "github.com/go-i2p/go-i2p/lib/common/data" "github.com/go-i2p/go-i2p/lib/crypto/curve25519" "github.com/go-i2p/go-i2p/lib/transport/ntcp/handshake" + "github.com/go-i2p/go-i2p/lib/transport/ntcp/kdf" "github.com/go-i2p/go-i2p/lib/transport/ntcp/messages" "github.com/samber/oops" + "golang.org/x/crypto/chacha20poly1305" ) func (s *NTCP2Session) CreateSessionRequest() (*messages.SessionRequest, error) { @@ -246,3 +248,40 @@ func (c *NTCP2Session) addDelayForSecurity() { delay := time.Duration(50+mrand.Intn(200)) * time.Millisecond time.Sleep(delay) } + +func (c *NTCP2Session) encryptSessionRequestOptions( + sessionRequestMessage *messages.SessionRequest, + obfuscatedX []byte, +) ([]byte, error) { + // Create KDF context + kdfContext := kdf.NewNTCP2KDF() + + // Perform DH and mix key + sharedSecret, err := c.computeSharedSecret(sessionRequestMessage.XContent[:], c.remoteStaticKey) + if err != nil { + return nil, oops.Errorf("failed to compute shared secret: %v", err) + } + + chacha20Key, err := kdfContext.MixKey(sharedSecret) + if err != nil { + return nil, oops.Errorf("failed to derive ChaCha20 key: %v", err) + } + + // Mix hash with ephemeral key + kdfContext.MixHash(obfuscatedX) + + // Create AEAD cipher + aead, err := chacha20poly1305.New(chacha20Key) + if err != nil { + return nil, oops.Errorf("failed to create ChaCha20-Poly1305 cipher: %v", err) + } + + // Prepare the nonce (all zeros for first message) + nonce := make([]byte, chacha20poly1305.NonceSize) + + // Encrypt options block using associated data + optionsData := sessionRequestMessage.Options.Data() + ciphertext := aead.Seal(nil, nonce, optionsData, obfuscatedX) + + return ciphertext, nil +} diff --git a/lib/transport/ntcp/session_request_new.go b/lib/transport/ntcp/session_request_new.go index 212ffcf..ad416b5 100644 --- a/lib/transport/ntcp/session_request_new.go +++ b/lib/transport/ntcp/session_request_new.go @@ -19,6 +19,17 @@ SessionRequestProcessor implements NTCP2 Message 1 (SessionRequest): 4. Encrypt options block using ChaCha20-Poly1305 5. Assemble final message: obfuscated X + encrypted options + padding 6. Write complete message to connection + +SessionRequestProcessor processes incoming NTCP2 Message 1 (SessionRequest): +1. Read and buffer the fixed-length ephemeral key portion (X) +2. Deobfuscate X using AES with local router hash as key +3. Validate the ephemeral key (X) is a valid Curve25519 point +4. Read the ChaCha20-Poly1305 encrypted options block +5. Derive KDF for handshake message 1 using X and local static key +6. Decrypt and authenticate the options block +7. Extract and validate handshake parameters (timestamp, version, padding length) +8. Read and validate any padding bytes +9. Check timestamp for acceptable clock skew (±60 seconds?) */ type SessionRequestProcessor struct { *NTCP2Session