mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 17:05:19 +00:00
185 lines
8.9 KiB
Swift
185 lines
8.9 KiB
Swift
import Foundation
|
|
|
|
/// QR verification scaffolding: schema, signing, and basic challenge/response helpers.
|
|
final class VerificationService {
|
|
static let shared = VerificationService()
|
|
|
|
// Injected Noise service from the running transport (do NOT create new BLEService)
|
|
private var noise: NoiseEncryptionService?
|
|
func configure(with noise: NoiseEncryptionService) { self.noise = noise }
|
|
|
|
/// Encapsulates the data encoded into a verification QR
|
|
struct VerificationQR: Codable {
|
|
let v: Int
|
|
let noiseKeyHex: String
|
|
let signKeyHex: String
|
|
let npub: String?
|
|
let nickname: String
|
|
let ts: Int64
|
|
let nonceB64: String
|
|
var sigHex: String
|
|
|
|
static let context = "bitchat-verify-v1"
|
|
|
|
/// Canonical bytes used for signature (deterministic ordering)
|
|
func canonicalBytes() -> Data {
|
|
var out = Data()
|
|
func appendField(_ s: String) {
|
|
let d = s.data(using: .utf8) ?? Data()
|
|
out.append(UInt8(min(d.count, 255)))
|
|
out.append(d.prefix(255))
|
|
}
|
|
appendField(Self.context)
|
|
appendField(String(v))
|
|
appendField(noiseKeyHex.lowercased())
|
|
appendField(signKeyHex.lowercased())
|
|
appendField(npub ?? "")
|
|
appendField(nickname)
|
|
appendField(String(ts))
|
|
appendField(nonceB64)
|
|
return out
|
|
}
|
|
|
|
func toURLString() -> String {
|
|
var comps = URLComponents()
|
|
comps.scheme = "bitchat"
|
|
comps.host = "verify"
|
|
comps.queryItems = [
|
|
URLQueryItem(name: "v", value: String(v)),
|
|
URLQueryItem(name: "noise", value: noiseKeyHex),
|
|
URLQueryItem(name: "sign", value: signKeyHex),
|
|
URLQueryItem(name: "nick", value: nickname),
|
|
URLQueryItem(name: "ts", value: String(ts)),
|
|
URLQueryItem(name: "nonce", value: nonceB64),
|
|
URLQueryItem(name: "sig", value: sigHex)
|
|
] + (npub != nil ? [URLQueryItem(name: "npub", value: npub)] : [])
|
|
return comps.string ?? ""
|
|
}
|
|
|
|
static func fromURL(_ url: URL) -> VerificationQR? {
|
|
guard url.scheme == "bitchat", url.host == "verify",
|
|
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems else { return nil }
|
|
func val(_ name: String) -> String? { items.first(where: { $0.name == name })?.value }
|
|
guard let vStr = val("v"), let v = Int(vStr),
|
|
let noise = val("noise"), let sign = val("sign"),
|
|
let nick = val("nick"), let tsStr = val("ts"), let ts = Int64(tsStr),
|
|
let nonce = val("nonce"), let sig = val("sig") else { return nil }
|
|
return VerificationQR(v: v, noiseKeyHex: noise, signKeyHex: sign, npub: val("npub"), nickname: nick, ts: ts, nonceB64: nonce, sigHex: sig)
|
|
}
|
|
}
|
|
|
|
// MARK: - Public API
|
|
|
|
/// Build a signed QR string for the current identity
|
|
func buildMyQRString(nickname: String, npub: String?) -> String? {
|
|
// Simple short-lived cache to speed up sheet opening
|
|
struct Cache { static var last: (nick: String, npub: String?, builtAt: Date, value: String)? }
|
|
if let c = Cache.last, c.nick == nickname, c.npub == npub, Date().timeIntervalSince(c.builtAt) < 60 {
|
|
return c.value
|
|
}
|
|
guard let noise = noise else { return nil }
|
|
let noiseKey = noise.getStaticPublicKeyData().hexEncodedString()
|
|
let signKey = noise.getSigningPublicKeyData().hexEncodedString()
|
|
let ts = Int64(Date().timeIntervalSince1970)
|
|
var nonce = Data(count: 16)
|
|
_ = nonce.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 16, $0.baseAddress!) }
|
|
let nonceB64 = nonce.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
|
|
let payload = VerificationQR(v: 1, noiseKeyHex: noiseKey, signKeyHex: signKey, npub: npub, nickname: nickname, ts: ts, nonceB64: nonceB64, sigHex: "")
|
|
let msg = payload.canonicalBytes()
|
|
guard let sig = noise.signData(msg) else { return nil }
|
|
let signed = VerificationQR(v: payload.v,
|
|
noiseKeyHex: payload.noiseKeyHex,
|
|
signKeyHex: payload.signKeyHex,
|
|
npub: payload.npub,
|
|
nickname: payload.nickname,
|
|
ts: payload.ts,
|
|
nonceB64: payload.nonceB64,
|
|
sigHex: sig.hexEncodedString())
|
|
let out = signed.toURLString()
|
|
Cache.last = (nickname, npub, Date(), out)
|
|
return out
|
|
}
|
|
|
|
/// Verify a scanned QR and return the parsed payload if valid (signature + freshness checks)
|
|
func verifyScannedQR(_ urlString: String, maxAge: TimeInterval = TransportConfig.verificationQRMaxAgeSeconds) -> VerificationQR? {
|
|
guard let url = URL(string: urlString), let qr = VerificationQR.fromURL(url) else { return nil }
|
|
// Freshness
|
|
let now = Date().timeIntervalSince1970
|
|
if now - Double(qr.ts) > maxAge { return nil }
|
|
// Verify signature using embedded ed25519 signKey
|
|
guard let sig = Data(hexString: qr.sigHex), let signKey = Data(hexString: qr.signKeyHex) else { return nil }
|
|
guard let noise = noise else { return nil }
|
|
let ok = noise.verifySignature(sig, for: qr.canonicalBytes(), publicKey: signKey)
|
|
return ok ? qr : nil
|
|
}
|
|
|
|
// MARK: - Noise payloads (scaffold only)
|
|
|
|
func buildVerifyChallenge(noiseKeyHex: String, nonceA: Data) -> Data {
|
|
// TLV: [0x01 len noiseKeyHex ascii] [0x02 len nonceA]
|
|
var tlv = Data()
|
|
let n0: [UInt8] = [0x01, UInt8(min(noiseKeyHex.count, 255))]
|
|
tlv.append(contentsOf: n0)
|
|
tlv.append(noiseKeyHex.data(using: .utf8)!.prefix(255))
|
|
tlv.append(0x02)
|
|
tlv.append(UInt8(min(nonceA.count, 255)))
|
|
tlv.append(nonceA.prefix(255))
|
|
return NoisePayload(type: .verifyChallenge, data: tlv).encode()
|
|
}
|
|
|
|
func buildVerifyResponse(noiseKeyHex: String, nonceA: Data) -> Data? {
|
|
// Sign context: verify-response | noiseKeyHex | nonceA
|
|
var msg = Data("bitchat-verify-resp-v1".utf8)
|
|
let nk = noiseKeyHex.data(using: .utf8) ?? Data()
|
|
msg.append(UInt8(min(nk.count, 255))); msg.append(nk.prefix(255))
|
|
msg.append(nonceA)
|
|
guard let noise = noise, let sig = noise.signData(msg) else { return nil }
|
|
var tlv = Data()
|
|
tlv.append(0x01); tlv.append(UInt8(min(nk.count, 255))); tlv.append(nk.prefix(255))
|
|
tlv.append(0x02); tlv.append(UInt8(min(nonceA.count, 255))); tlv.append(nonceA.prefix(255))
|
|
tlv.append(0x03); tlv.append(UInt8(min(sig.count, 255))); tlv.append(sig.prefix(255))
|
|
return NoisePayload(type: .verifyResponse, data: tlv).encode()
|
|
}
|
|
|
|
func parseVerifyChallenge(_ data: Data) -> (noiseKeyHex: String, nonceA: Data)? {
|
|
var idx = 0
|
|
func take(_ n: Int) -> Data? {
|
|
guard idx + n <= data.count else { return nil }
|
|
let d = data[idx..<(idx+n)]
|
|
idx += n
|
|
return Data(d)
|
|
}
|
|
// Expect type already stripped; we receive only TLV here
|
|
// TLV 0x01 noiseKeyHex
|
|
guard let t1 = take(1), t1[0] == 0x01, let l1 = take(1), let s1 = take(Int(l1[0])),
|
|
let noiseStr = String(data: s1, encoding: .utf8) else { return nil }
|
|
// TLV 0x02 nonceA
|
|
guard let t2 = take(1), t2[0] == 0x02, let l2 = take(1), let nA = take(Int(l2[0])) else { return nil }
|
|
return (noiseStr, nA)
|
|
}
|
|
|
|
func parseVerifyResponse(_ data: Data) -> (noiseKeyHex: String, nonceA: Data, signature: Data)? {
|
|
var idx = 0
|
|
func take(_ n: Int) -> Data? {
|
|
guard idx + n <= data.count else { return nil }
|
|
let d = data[idx..<(idx+n)]
|
|
idx += n
|
|
return Data(d)
|
|
}
|
|
guard let t1 = take(1), t1[0] == 0x01, let l1 = take(1), let s1 = take(Int(l1[0])),
|
|
let noiseStr = String(data: s1, encoding: .utf8) else { return nil }
|
|
guard let t2 = take(1), t2[0] == 0x02, let l2 = take(1), let nA = take(Int(l2[0])) else { return nil }
|
|
guard let t3 = take(1), t3[0] == 0x03, let l3 = take(1), let sig = take(Int(l3[0])) else { return nil }
|
|
return (noiseStr, nA, sig)
|
|
}
|
|
|
|
func verifyResponseSignature(noiseKeyHex: String, nonceA: Data, signature: Data, signerPublicKeyHex: String) -> Bool {
|
|
var msg = Data("bitchat-verify-resp-v1".utf8)
|
|
let nk = noiseKeyHex.data(using: .utf8) ?? Data()
|
|
msg.append(UInt8(min(nk.count, 255))); msg.append(nk.prefix(255))
|
|
msg.append(nonceA)
|
|
guard let noise = noise, let pub = Data(hexString: signerPublicKeyHex) else { return false }
|
|
return noise.verifySignature(signature, for: msg, publicKey: pub)
|
|
}
|
|
}
|