p2p-swift / Sources / IrdP2PCrypto / Identity.swift
607 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
import Foundation
import CryptoKit

/// A user's long-lived identity: a 32-byte Ed25519 seed (43 URL-safe base64
/// characters). Optional in the protocol โ€” rooms work without it โ€” but it lets
/// peers recognise each other across sessions by fingerprint.
public struct Identity: Sendable {
    private let privateKey: Curve25519.Signing.PrivateKey
    /// Display name announced in the room hello (clamped by the receiver).
    public var name: String?
    /// Hue 0..<360 announced in the room hello.
    public var hue: Int?
    /// A single glyph (emoji) announced in the room hello.
    public var glyph: String?

    public static func generate() -> Identity {
        Identity(privateKey: Curve25519.Signing.PrivateKey())
    }

    /// Rebuilds the identity from its 32-byte seed in URL-safe base64.
    public init(seedBase64URL: String) throws {
        guard let seed = Bytes.fromBase64URL(seedBase64URL), seed.count == 32,
              let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: seed)
        else { throw IdentifierError.malformedSeed }
        self.init(privateKey: key)
    }

    private init(privateKey: Curve25519.Signing.PrivateKey) {
        self.privateKey = privateKey
    }

    /// The seed, for backup and device transfer. Treat it as a password.
    public var seedBase64URL: String { Bytes.base64URL(privateKey.rawRepresentation) }
    public var publicKey: Data { privateKey.publicKey.rawRepresentation }
    public var publicKeyBase64URL: String { Bytes.base64URL(publicKey) }
    public var fingerprint: Fingerprint { Fingerprint(publicKey: publicKey) }

    /// Signs the room-session binding `sueta-id|v2|<appSalt>|<roomId>|<peerId>`.
    /// Returns standard base64 (the reference encoding for assertions).
    public func assert(appSalt: String, roomId: RoomId, peerId: PeerId) -> String {
        Bytes.base64(signRaw(Identity.bindingBytes(appSalt: appSalt, roomId: roomId.string, peerId: peerId.hex)))
    }

    /// Raw Ed25519 signature (64 bytes).
    public func signRaw(_ data: Data) -> Data {
        // Ed25519 signing cannot fail for a valid key.
        try! privateKey.signature(for: data)
    }

    static func bindingBytes(appSalt: String, roomId: String, peerId: String) -> Data {
        Data("sueta-id|v2|\(appSalt)|\(roomId)|\(peerId)".utf8)
    }

    /// Verifies a peer's assertion; the fingerprint of the key on success, `nil` otherwise.
    /// Accepts the signature in standard or URL-safe base64.
    public static func verifyAssertion(appSalt: String, roomId: RoomId, peerId: PeerId,
                                       publicKeyBase64URL: String, signature: String) -> Fingerprint? {
        verifyAssertion(appSalt: appSalt, roomId: roomId.string, peerId: peerId.hex,
                        publicKeyBase64URL: publicKeyBase64URL, signature: signature)
    }

    static func verifyAssertion(appSalt: String, roomId: String, peerId: String,
                                publicKeyBase64URL: String, signature: String) -> Fingerprint? {
        guard let pub = Bytes.fromBase64URL(publicKeyBase64URL),
              let key = try? Curve25519.Signing.PublicKey(rawRepresentation: pub),
              let sig = Bytes.fromBase64URL(signature.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_")),
              key.isValidSignature(sig, for: bindingBytes(appSalt: appSalt, roomId: roomId, peerId: peerId))
        else { return nil }
        return Fingerprint(publicKey: pub)
    }

    /// Verifies a raw signature (URL-safe base64) over arbitrary bytes.
    public static func verifyRaw(publicKeyBase64URL: String, signatureBase64URL: String, data: Data) -> Bool {
        guard let pub = Bytes.fromBase64URL(publicKeyBase64URL),
              let key = try? Curve25519.Signing.PublicKey(rawRepresentation: pub),
              let sig = Bytes.fromBase64URL(signatureBase64URL)
        else { return false }
        return key.isValidSignature(sig, for: data)
    }
}

/// SHA-256 of a public key: the full hex digest plus four emoji from its first
/// 24 bits, for humans to compare.
public struct Fingerprint: Sendable, Hashable, CustomStringConvertible {
    public let emoji: String
    public let hex: String

    /// The 64-entry table is part of the wire format; several entries carry a
    /// variation selector (U+FE0F) and must stay exactly as they are.
    public static let emojiTable: [String] = [
        "๐Ÿข", "๐ŸฆŠ", "๐Ÿผ", "๐Ÿฆ", "๐Ÿธ", "๐Ÿ™", "๐Ÿฆ‰", "๐Ÿ", "๐Ÿฌ", "๐Ÿฆ„", "๐Ÿž", "๐Ÿฆ‹", "๐ŸŒต", "๐ŸŒฒ", "๐Ÿ", "๐Ÿ„",
        "๐ŸŒป", "๐ŸŒ™", "โญ", "๐Ÿ”ฅ", "๐ŸŒˆ", "โšก", "โ„๏ธ", "๐ŸŒŠ", "๐ŸŽ", "๐Ÿ‹", "๐Ÿ‡", "๐Ÿ“", "๐Ÿฅ‘", "๐ŸŒฝ", "๐Ÿฅ•", "๐Ÿฉ",
        "๐Ÿ•", "๐Ÿฅจ", "๐Ÿง€", "๐Ÿฟ", "โ˜•", "๐Ÿต", "๐ŸงŠ", "๐ŸŽˆ", "๐ŸŽฒ", "๐ŸŽฏ", "๐ŸŽธ", "๐ŸŽบ", "๐Ÿฅ", "๐Ÿš€", "๐Ÿšฒ", "โ›ต",
        "๐Ÿš‚", "๐Ÿ—ฟ", "๐Ÿ—๏ธ", "๐Ÿ””", "๐Ÿงฒ", "๐Ÿงญ", "โณ", "๐Ÿ“š", "๐Ÿ–๏ธ", "๐Ÿ“Ž", "โœ‚๏ธ", "๐Ÿ”", "๐Ÿ’Ž", "๐Ÿ›ก๏ธ", "โš™๏ธ", "๐ŸŽ",
    ]

    public init(publicKey: Data) {
        let h = Bytes.sha256(publicKey)
        let bits = (UInt32(h[0]) << 16) | (UInt32(h[1]) << 8) | UInt32(h[2])
        let t = Fingerprint.emojiTable
        emoji = t[Int((bits >> 18) & 63)] + t[Int((bits >> 12) & 63)] + t[Int((bits >> 6) & 63)] + t[Int(bits & 63)]
        hex = Bytes.hex(h)
    }

    public var description: String { emoji }
}

static mirror of HEAD ยท about ยท clone: git clone https://ird.cod-sursa.ro/p2p-swift.git