iOS new: Identity.swift
This commit is contained in:
parent
e8af03f888
commit
dfed314ac3
193
Montana-iOS/Montana Messenger/Montana Messenger/Identity.swift
Normal file
193
Montana-iOS/Montana Messenger/Montana Messenger/Identity.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user