Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.5
// swift-tools-version:6.0
//===----------------------------------------------------------------------===//
//
// This source file is part of the swift-libp2p open source project
Expand Down Expand Up @@ -32,13 +32,13 @@ let package = Package(
// Dependencies declare other packages that this package depends on.

// Swift NIO for all things networking
.package(url: "https://github.com/apple/swift-nio-extras.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/apple/swift-nio-extras.git", .upToNextMajor(from: "1.25.0")),

// LibP2P Core Modules
.package(url: "https://github.com/swift-libp2p/swift-libp2p.git", .upToNextMinor(from: "0.2.0")),
.package(url: "https://github.com/swift-libp2p/swift-libp2p.git", .upToNextMinor(from: "0.3.0")),

// Noise (Security Protocol)
.package(url: "https://github.com/swift-libp2p/swift-noise.git", .upToNextMinor(from: "0.0.1")),
.package(url: "https://github.com/swift-libp2p/swift-noise.git", .upToNextMinor(from: "0.1.0")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand Down
10 changes: 5 additions & 5 deletions Sources/LibP2PNoise/ChannelHandlers/DecryptionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,26 @@
//
//===----------------------------------------------------------------------===//

import Crypto
import Logging
import NIOCore
import Noise

// Noise XX Outbound Data Encrypter
internal final class InboundNoiseDecryptionHandler: ChannelInboundHandler {
internal final class InboundNoiseDecryptionHandler: ChannelInboundHandler, Sendable {
public typealias InboundIn = ByteBuffer //Encrypted ciphertext data
public typealias InboundOut = ByteBuffer //Plaintext data

/// Do we need to encrypt and decrypt with AD? Or can we just use the CipherState without the running Hash (h)?
/// The JS implementation just passes an empty buffer into the AD. Let's try the same...
private let cs: Noise.CipherState
private var logger: Logger
private let logger: Logger

public init(cipherState: Noise.CipherState, logger: Logger) {
var logger = logger
logger[metadataKey: "NOISE"] = .string("Decrypter")

self.logger = logger
self.cs = cipherState

self.logger[metadataKey: "NOISE"] = .string("Decrypter")
}

public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
Expand Down
11 changes: 5 additions & 6 deletions Sources/LibP2PNoise/ChannelHandlers/EncryptionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,26 @@
//
//===----------------------------------------------------------------------===//

import Crypto
import LibP2PCore
import Logging
import NIOCore
import Noise

// Noise XX Outbound Data Encrypter
internal final class OutboundNoiseEncryptionHandler: ChannelOutboundHandler {
internal final class OutboundNoiseEncryptionHandler: ChannelOutboundHandler, Sendable {
public typealias OutboundIn = ByteBuffer //Plaintext data
public typealias OutboundOut = ByteBuffer //Encrypted Ciphertext data

/// Do we need to encrypt and decrypt with AD? Or can we just use the CipherState without the running Hash (h)?
/// The JS implementation just passes an empty buffer into the AD. Let's try the same...
private let cs: Noise.CipherState
private var logger: Logger
private let logger: Logger

public init(cipherState: Noise.CipherState, logger: Logger) {
var logger = logger
logger[metadataKey: "NOISE"] = .string("Encrypter")

self.logger = logger
self.cs = cipherState

self.logger[metadataKey: "NOISE"] = .string("Encrypter")
}

public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
Expand Down
110 changes: 67 additions & 43 deletions Sources/LibP2PNoise/ChannelHandlers/HandshakeHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,21 @@
//
//===----------------------------------------------------------------------===//

import Crypto
// TODO: Remove preconcurrency tag once we drop support for swift 6.0
@preconcurrency import Crypto
import Foundation
import LibP2PCore
import Logging
import NIOConcurrencyHelpers
import NIOCore
import NIOExtras
import Noise
import PeerID

public enum NoiseErrors: Error {
case invalidNoiseHandshakeMessage
case remotePeerMismatch
case invalidSignature
case failedToInstantiateCipherStates
case invalidIdentityKey
case invalidRemoteStaticKey
case invalidSignaturePrefix
}

/// Noise XX
///
/// Should we have a seperate Handler responsible for the Handshake that installs the Encrypter and Decrypter once complete?
internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, RemovableChannelHandler {
internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, RemovableChannelHandler, Sendable {
public typealias InboundIn = ByteBuffer //Noise Handshake Message, or Ciphertext post handkshake
public typealias InboundOut = ByteBuffer //Plaintext post handshake
public typealias OutboundOut = ByteBuffer //Noise Handshake Message
Expand All @@ -43,40 +35,69 @@ internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, Remova

private let payloadSigPrefix = "noise-libp2p-static-key:"

private enum State {
private enum State: Sendable {
case handshakeInProgress
case secured
}
private var state: State

private var state: State {
get { _state.withLockedValue { $0 } }
set { _state.withLockedValue { $0 = newValue } }
}
private let _state: NIOLockedValueBox<State>

private let handshakeState: Noise.HandshakeState
private let staticNoiseKey: Curve25519.KeyAgreement.PrivateKey

private var logger: Logger
private let logger: Logger
private let localPeerInfo: PeerID
private var remotePeerInfo: PeerID? = nil
private var expectedRemotePeerID: String? = nil

private var remotePeerInfo: PeerID? {
get { _remotePeerInfo.withLockedValue { $0 } }
set { _remotePeerInfo.withLockedValue { $0 = newValue } }
}
private let _remotePeerInfo: NIOLockedValueBox<PeerID?>

private var expectedRemotePeerID: PeerID? {
get { _expectedRemotePeerID.withLockedValue { $0 } }
set { _expectedRemotePeerID.withLockedValue { $0 = newValue } }
}
private let _expectedRemotePeerID: NIOLockedValueBox<PeerID?>

private let mode: LibP2PCore.Mode

private var messagesWritten: Int = 0
private var lengthEncoder: LengthFieldPrepender
private var lengthDecoder: LengthFieldBasedFrameDecoder
private var messagesWritten: Int {
get { _messagesWritten.withLockedValue { $0 } }
set { _messagesWritten.withLockedValue { $0 = newValue } }
}
private let _messagesWritten: NIOLockedValueBox<Int> = .init(0)

private let lengthEncoder: LengthFieldPrepender
private let lengthDecoder: LengthFieldBasedFrameDecoder

private var shouldWarn: Bool = false
private var shouldWarn: Bool {
get { _shouldWarn.withLockedValue { $0 } }
set { _shouldWarn.withLockedValue { $0 = newValue } }
}
private let _shouldWarn: NIOLockedValueBox<Bool> = .init(false)

/// - TODO: Include a param for the Remote PeerID when we're the dialer so we can compare the NoiseHandshakePayload public key to the peer dialed.
public init(
peerID: PeerID,
mode: LibP2PCore.Mode,
logger: Logger,
secured: EventLoopPromise<Connection.SecuredResult>,
expectedRemotePeerID: String?
expectedRemotePeerID: PeerID?
) {
self.localPeerInfo = peerID
self.remotePeerInfo = nil
self.expectedRemotePeerID = expectedRemotePeerID
self.state = .handshakeInProgress
self._remotePeerInfo = .init(nil)
self._expectedRemotePeerID = .init(expectedRemotePeerID)
self._state = .init(.handshakeInProgress)

var logger = logger
logger[metadataKey: "NOISE"] = .string("\(mode.rawValue)")
self.logger = logger

self.mode = mode

// An MSS Callback that we can use to notify it once the handshake is complete and the channel is secured
Expand All @@ -101,8 +122,6 @@ internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, Remova

self.lengthDecoder = LengthFieldBasedFrameDecoder(lengthFieldBitLength: .twoBytes, lengthFieldEndianness: .big)
self.lengthEncoder = LengthFieldPrepender(lengthFieldBitLength: .twoBytes, lengthFieldEndianness: .big)

self.logger[metadataKey: "NOISE"] = .string("\(mode.rawValue)")
}

public func handlerAdded(context: ChannelHandlerContext) {
Expand Down Expand Up @@ -156,12 +175,12 @@ internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, Remova
logger.error(
"Invalid Noise Handshake Message Received. Aborting Handshake and closing connection..."
)
return abort(context: context, error: NoiseErrors.invalidNoiseHandshakeMessage)
return abort(context: context, error: NoiseUpgrader.Error.invalidNoiseHandshakeMessage)
}

// Reconstruct Listeners Handshake Payload
//logger.info("Attempting to decode NoiseHandshakePayload")
let lnhp = try NoiseHandshakePayload(contiguousBytes: payload)
let lnhp = try NoiseHandshakePayload(serializedBytes: payload)
//logger.info("Attempting to instantiate Remote PeerID from NoiseHandshakePayload IdentityKey")
//logger.info("Identity Key: \(lnhp.identityKey.asString(base: .base16))")

Expand All @@ -180,26 +199,28 @@ internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, Remova
guard remote.b58String == rpi.b58String else {
//logger.error("Listeners Noise Handshake Identity Key does not match the Peer we dialed. Aborting Handshake and closing connection... (RemotePeerInfo)")
//logger.error("\(remote.b58String) =/= \(rpi.b58String)" )
return abort(context: context, error: NoiseErrors.remotePeerMismatch)
return abort(context: context, error: NoiseUpgrader.Error.remotePeerMismatch)
}
logger.trace(
"Validated the dialed peer! \(rpi.b58String) is in fact who they claim to be..."
)
} else if let remoteID = expectedRemotePeerID, let rid = try? PeerID(cid: remoteID) {
guard rid == rpi else {
} else if let expectedRemotePeerID {
guard expectedRemotePeerID == rpi else {
logger.error(
"Listeners Noise Handshake Identity Key does not match the Peer we dialed. Aborting Handshake and closing connection...(ExpectedRemotePeerID)"
)
logger.error("Expected: b58: \(rid.b58String), cid: \(rid.cidString)")
logger.error(
"Expected: b58: \(expectedRemotePeerID.b58String), cid: \(expectedRemotePeerID.cidString)"
)
logger.error("=/=")
logger.error("Provided: b58: \(rpi.b58String), cid: \(rpi.cidString)")
logger.error(
"Expected Key Type: \(rid.type), \(String(describing: rid.keyPair?.keyType))"
"Expected Key Type: \(expectedRemotePeerID.type), \(String(describing: expectedRemotePeerID.keyPair?.keyType))"
)
logger.error(
"Provided Key Type: \(rpi.type), \(String(describing: rpi.keyPair?.keyType))"
)
return abort(context: context, error: NoiseErrors.remotePeerMismatch)
return abort(context: context, error: NoiseUpgrader.Error.remotePeerMismatch)
}
logger.trace(
"Validated the dialed peer! \(rpi.b58String) is in fact who they claim to be..."
Expand All @@ -220,7 +241,7 @@ internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, Remova
logger.error(
"Listeners Noise Handshake Signature Verification failed. Aborting Handshake and closing connection..."
)
return abort(context: context, error: NoiseErrors.invalidSignature)
return abort(context: context, error: NoiseUpgrader.Error.invalidSignature)
}

// If we made it this far, then everything checks out!
Expand All @@ -238,7 +259,7 @@ internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, Remova
logger.error(
"Failed to instantiate CipherStates after processing message C. Aborting Handshake and closing connection..."
)
return abort(context: context, error: NoiseErrors.failedToInstantiateCipherStates)
return abort(context: context, error: NoiseUpgrader.Error.failedToInstantiateCipherStates)
}

context.writeAndFlush(
Expand Down Expand Up @@ -340,35 +361,35 @@ internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, Remova
logger.error(
"Failed to instantiate CipherStates after processing message C. Aborting Handshake and closing connection..."
)
return abort(context: context, error: NoiseErrors.failedToInstantiateCipherStates)
return abort(context: context, error: NoiseUpgrader.Error.failedToInstantiateCipherStates)
}

// Verify the initiators signature payload with their Public PeerID
//logger.info("Verifying NoiesHandshakePayload")
let inhp = try NoiseHandshakePayload(contiguousBytes: payload)
let inhp = try NoiseHandshakePayload(serializedBytes: payload)

// Initiate Remote PeerID from the payloads identityKey
guard let rpid = try? PeerID(marshaledPublicKey: inhp.identityKey) else {
logger.error(
"Could not instantiate PeerID from Initiators Identity Key. Aborting Handshake and closing connection..."
)
return abort(context: context, error: NoiseErrors.invalidIdentityKey)
return abort(context: context, error: NoiseUpgrader.Error.invalidIdentityKey)
}
guard let remoteStatic = try? self.handshakeState.peerStatic() else {
logger.error("Failed to access remote peers static noise key")
return abort(context: context, error: NoiseErrors.invalidRemoteStaticKey)
return abort(context: context, error: NoiseUpgrader.Error.invalidRemoteStaticKey)
}
// Construct the data we expect the signature to be valid for
guard let sigPrefix = payloadSigPrefix.data(using: .utf8) else {
logger.error("Invalid Signature Prefix")
return abort(context: context, error: NoiseErrors.invalidSignaturePrefix)
return abort(context: context, error: NoiseUpgrader.Error.invalidSignaturePrefix)
}
let expectedSignedData = sigPrefix + remoteStatic.rawRepresentation
guard try rpid.isValidSignature(inhp.identitySig, for: expectedSignedData) else {
logger.error(
"Initiators Noise Handshake Signature Verification failed. Aborting Handshake and closing connection..."
)
return abort(context: context, error: NoiseErrors.invalidSignature)
return abort(context: context, error: NoiseUpgrader.Error.invalidSignature)
}

// Upgrade the channel with the encrypter / decrypter handlers
Expand Down Expand Up @@ -495,3 +516,6 @@ internal final class InboundNoiseHandshakeHandler: ChannelInboundHandler, Remova
context.close(mode: .all, promise: nil)
}
}

extension LengthFieldPrepender: @retroactive @unchecked Sendable {}
extension LengthFieldBasedFrameDecoder: @retroactive @unchecked Sendable {}
25 changes: 25 additions & 0 deletions Sources/LibP2PNoise/Errors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the swift-libp2p open source project
//
// Copyright (c) 2022-2025 swift-libp2p project authors
// Licensed under MIT
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of swift-libp2p project authors
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

extension NoiseUpgrader {
public enum Error: Swift.Error, Sendable {
case invalidNoiseHandshakeMessage
case remotePeerMismatch
case invalidSignature
case failedToInstantiateCipherStates
case invalidIdentityKey
case invalidRemoteStaticKey
case invalidSignaturePrefix
}
}
2 changes: 1 addition & 1 deletion Sources/LibP2PNoise/LibP2PNoise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct NoiseUpgrader: SecurityUpgrader {
mode: conn.mode,
logger: conn.logger,
secured: securedPromise,
expectedRemotePeerID: conn.expectedRemotePeer?.b58String
expectedRemotePeerID: conn.expectedRemotePeer
)

return conn.channel.pipeline.addHandler(handshake, position: position)
Expand Down
Loading