Files
umn/Sources/UltraMeshCore/Crypto.swift
T
2026-08-31 15:55:09 -05:00

80 lines
4.9 KiB
Swift

import Foundation
import CryptoKit
public final class NodeIdentity: @unchecked Sendable {
public static let currentVersion = 1
public let signingKey: Curve25519.Signing.PrivateKey
public let agreementKey: Curve25519.KeyAgreement.PrivateKey
public let record: NodeRecord
public init(signingKey: Curve25519.Signing.PrivateKey = .init(),
agreementKey: Curve25519.KeyAgreement.PrivateKey = .init()) throws {
self.signingKey = signingKey; self.agreementKey = agreementKey
let signing = signingKey.publicKey.rawRepresentation
self.record = NodeRecord(address: try MeshAddress.derive(from: signing),
signingPublicKey: signing,
agreementPublicKey: agreementKey.publicKey.rawRepresentation)
}
public static func loadOrCreate(at url: URL) throws -> NodeIdentity {
struct Stored: Codable { let version: Int; let signing: Data; let agreement: Data }
let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: url) {
let value = try decoder.decode(Stored.self, from: data)
guard value.version == currentVersion else { throw UMNError.invalidIdentity }
return try NodeIdentity(signingKey: .init(rawRepresentation: value.signing),
agreementKey: .init(rawRepresentation: value.agreement))
}
let identity = try NodeIdentity()
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
let encoder = PropertyListEncoder(); encoder.outputFormat = .binary
let data = try encoder.encode(Stored(version: currentVersion,
signing: identity.signingKey.rawRepresentation,
agreement: identity.agreementKey.rawRepresentation))
try data.write(to: url, options: .atomic)
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
return identity
}
public func sign(_ data: Data) throws -> Data { try signingKey.signature(for: data) }
public func makeLinkState(sequence: UInt64, neighbors: [MeshAddress]) throws -> LinkState {
let unsigned = LinkState(origin: record, sequence: sequence, neighbors: neighbors, signature: Data())
return LinkState(origin: record, sequence: sequence, neighbors: neighbors,
signature: try sign(unsigned.signingBytes()))
}
public func seal(_ inner: InnerFrame, to destination: NodeRecord) throws -> SealedPayload {
guard destination.validate() else { throw UMNError.invalidIdentity }
let encoder = PropertyListEncoder(); encoder.outputFormat = .binary
let innerData = try encoder.encode(inner)
struct Signed: Codable { let inner: Data; let signature: Data }
let signed = try encoder.encode(Signed(inner: innerData, signature: sign(innerData)))
let ephemeral = Curve25519.KeyAgreement.PrivateKey()
let remote = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: destination.agreementPublicKey)
let secret = try ephemeral.sharedSecretFromKeyAgreement(with: remote)
let key = secret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: Data("umn-e2e-v1".utf8),
sharedInfo: destination.address.bytes, outputByteCount: 32)
let box = try ChaChaPoly.seal(signed, using: key)
return SealedPayload(ephemeralPublicKey: ephemeral.publicKey.rawRepresentation,
combinedCiphertext: box.combined)
}
public func open(_ payload: SealedPayload, expectedSource: MeshAddress) throws -> InnerFrame {
struct Signed: Codable { let inner: Data; let signature: Data }
let ephemeral = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: payload.ephemeralPublicKey)
let secret = try agreementKey.sharedSecretFromKeyAgreement(with: ephemeral)
let key = secret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: Data("umn-e2e-v1".utf8),
sharedInfo: record.address.bytes, outputByteCount: 32)
let box = try ChaChaPoly.SealedBox(combined: payload.combinedCiphertext)
let data = try ChaChaPoly.open(box, using: key)
let decoder = PropertyListDecoder()
let signed = try decoder.decode(Signed.self, from: data)
let inner = try decoder.decode(InnerFrame.self, from: signed.inner)
guard inner.sourceRecord.address == expectedSource, inner.sourceRecord.validate() else { throw UMNError.invalidIdentity }
let keyVerify = try Curve25519.Signing.PublicKey(rawRepresentation: inner.sourceRecord.signingPublicKey)
guard keyVerify.isValidSignature(signed.signature, for: signed.inner) else { throw UMNError.invalidIdentity }
return inner
}
}