iOS new: Identity.swift

This commit is contained in:
efir369999 2026-05-05 17:16:09 +03:00
parent e8af03f888
commit dfed314ac3

View File

@ -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
}
}
}