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