From dfed314ac370b848db64fa20f7f841034ed8b59d Mon Sep 17 00:00:00 2001 From: efir369999 Date: Tue, 5 May 2026 17:16:09 +0300 Subject: [PATCH] iOS new: Identity.swift --- .../Montana Messenger/Identity.swift | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 Montana-iOS/Montana Messenger/Montana Messenger/Identity.swift diff --git a/Montana-iOS/Montana Messenger/Montana Messenger/Identity.swift b/Montana-iOS/Montana Messenger/Montana Messenger/Identity.swift new file mode 100644 index 0000000..a9eef60 --- /dev/null +++ b/Montana-iOS/Montana Messenger/Montana Messenger/Identity.swift @@ -0,0 +1,193 @@ +import Foundation +import Combine +import CryptoKit +import Security + +struct Identity { + let accountID: String + let displayName: String + let signingKey: Curve25519.Signing.PrivateKey + let agreementKey: Curve25519.KeyAgreement.PrivateKey + + var edPublicKeyB64: String { signingKey.publicKey.rawRepresentation.b64uNoPad } + var xPublicKeyB64: String { agreementKey.publicKey.rawRepresentation.b64uNoPad } + var edSeedB64: String { signingKey.rawRepresentation.b64uNoPad } + var xSeedB64: String { agreementKey.rawRepresentation.b64uNoPad } +} + +extension Data { + var b64uNoPad: String { + base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + static func fromB64uNoPad(_ s: String) -> Data? { + var t = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + while t.count % 4 != 0 { t += "=" } + return Data(base64Encoded: t) + } +} + +@MainActor +final class IdentityManager: ObservableObject { + static let shared = IdentityManager() + + @Published private(set) var identity: Identity? + + private let service = "network.montana.identity" + + init() { load() } + + func load() { + guard + let edSeed = readKeychain(key: "ed_seed"), + let xSeed = readKeychain(key: "x_seed"), + let aid = readKeychain(key: "account_id"), + let nameD = readKeychain(key: "display_name"), + let ed = try? Curve25519.Signing.PrivateKey(rawRepresentation: edSeed), + let x = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: xSeed), + let aidStr = String(data: aid, encoding: .utf8), + let nameStr = String(data: nameD, encoding: .utf8) + else { + identity = nil + return + } + identity = Identity( + accountID: aidStr, displayName: nameStr, + signingKey: ed, agreementKey: x + ) + } + + func create(name: String) -> Identity { + let ed = Curve25519.Signing.PrivateKey() + let x = Curve25519.KeyAgreement.PrivateKey() + let edPub = ed.publicKey.rawRepresentation + let aid = SHA256.hash(data: edPub).prefix(8).map { String(format: "%02x", $0) }.joined() + + write(key: "ed_seed", data: ed.rawRepresentation) + write(key: "x_seed", data: x.rawRepresentation) + write(key: "account_id", data: Data(aid.utf8)) + write(key: "display_name", data: Data(name.utf8)) + + let id = Identity(accountID: aid, displayName: name, signingKey: ed, agreementKey: x) + identity = id + return id + } + + func restore(edSeedB64: String, xSeedB64: String, name: String) throws -> Identity { + guard + let edSeed = Data.fromB64uNoPad(edSeedB64), + let xSeed = Data.fromB64uNoPad(xSeedB64), + let ed = try? Curve25519.Signing.PrivateKey(rawRepresentation: edSeed), + let x = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: xSeed) + else { throw NSError(domain: "Identity", code: 1, userInfo: [NSLocalizedDescriptionKey: "Невалидный ключ"]) } + + let edPub = ed.publicKey.rawRepresentation + let aid = SHA256.hash(data: edPub).prefix(8).map { String(format: "%02x", $0) }.joined() + + write(key: "ed_seed", data: ed.rawRepresentation) + write(key: "x_seed", data: x.rawRepresentation) + write(key: "account_id", data: Data(aid.utf8)) + write(key: "display_name", data: Data(name.utf8)) + + let id = Identity(accountID: aid, displayName: name, signingKey: ed, agreementKey: x) + identity = id + return id + } + + func wipe() { + for k in ["ed_seed", "x_seed", "account_id", "display_name"] { + deleteKeychain(key: k) + } + identity = nil + } + + func setDisplayName(_ name: String) { + guard var id = identity else { return } + write(key: "display_name", data: Data(name.utf8)) + identity = Identity(accountID: id.accountID, displayName: name, + signingKey: id.signingKey, agreementKey: id.agreementKey) + _ = id + } + + private func readKeychain(key: String) -> Data? { + let q: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne, + ] + var out: CFTypeRef? + guard SecItemCopyMatching(q as CFDictionary, &out) == errSecSuccess, + let d = out as? Data else { return nil } + return d + } + + private func write(key: String, data: Data) { + let q: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + ] + SecItemDelete(q as CFDictionary) + var attrs = q + attrs[kSecValueData as String] = data + attrs[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + SecItemAdd(attrs as CFDictionary, nil) + } + + private func deleteKeychain(key: String) { + let q: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + ] + SecItemDelete(q as CFDictionary) + } +} + +enum MessageCrypto { + static func encrypt(_ plaintext: String, recipientXPubB64: String, mySigningKey: Curve25519.Signing.PrivateKey, myAgreementKey: Curve25519.KeyAgreement.PrivateKey) throws -> (nonceB64: String, ciphertextB64: String) { + guard let xPubData = Data.fromB64uNoPad(recipientXPubB64) else { + throw NSError(domain: "Crypto", code: 1) + } + let theirX = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: xPubData) + let shared = try myAgreementKey.sharedSecretFromKeyAgreement(with: theirX) + let key = shared.hkdfDerivedSymmetricKey(using: SHA256.self, + salt: Data("montana-mess-v1".utf8), + sharedInfo: Data(), + outputByteCount: 32) + let nonceData = SymmetricKey(size: .init(bitCount: 96)).withUnsafeBytes { Data($0) } + let nonce = try ChaChaPoly.Nonce(data: nonceData) + let sealed = try ChaChaPoly.seal(Data(plaintext.utf8), using: key, nonce: nonce) + return (nonceB64: nonceData.b64uNoPad, + ciphertextB64: (sealed.ciphertext + sealed.tag).b64uNoPad) + } + + static func decrypt(nonceB64: String, ciphertextB64: String, peerXPubB64: String, myAgreementKey: Curve25519.KeyAgreement.PrivateKey) -> String? { + guard + let nonceData = Data.fromB64uNoPad(nonceB64), + let combined = Data.fromB64uNoPad(ciphertextB64), + let xPubData = Data.fromB64uNoPad(peerXPubB64), + combined.count >= 16 + else { return nil } + do { + let theirX = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: xPubData) + let shared = try myAgreementKey.sharedSecretFromKeyAgreement(with: theirX) + let key = shared.hkdfDerivedSymmetricKey(using: SHA256.self, + salt: Data("montana-mess-v1".utf8), + sharedInfo: Data(), + outputByteCount: 32) + let nonce = try ChaChaPoly.Nonce(data: nonceData) + let ct = combined.prefix(combined.count - 16) + let tag = combined.suffix(16) + let box = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: ct, tag: tag) + let pt = try ChaChaPoly.open(box, using: key) + return String(data: pt, encoding: .utf8) + } catch { + return nil + } + } +}