p2p-swift / Sources / IrdP2P / Identity / IdentityStore.swift
462 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import Foundation
import IrdP2PCrypto
#if canImport(Security)
import Security
#endif

/// Where the identity seed lives between launches.
public protocol IdentityStore: Sendable {
    func load(account: String) throws -> Identity?
    func save(_ identity: Identity, account: String) throws
    func delete(account: String) throws
}

public enum IdentityStoreError: Error, Sendable, Equatable {
    case keychain(status: Int32)
    case corrupt
}

/// Keeps identities in memory only — for tests and previews.
public final class InMemoryIdentityStore: IdentityStore, @unchecked Sendable {
    private let lock = NSLock()
    private var seeds: [String: String] = [:]
    public init() {}
    public func load(account: String) throws -> Identity? {
        guard let seed = lock.withLock({ seeds[account] }) else { return nil }
        return try Identity(seedBase64URL: seed)
    }
    public func save(_ identity: Identity, account: String) throws { lock.withLock { seeds[account] = identity.seedBase64URL } }
    public func delete(account: String) throws { _ = lock.withLock { seeds.removeValue(forKey: account) } }
}

#if canImport(Security)
/// Stores the seed as a generic password item (this device only, after first unlock).
public struct KeychainIdentityStore: IdentityStore {
    public var service: String
    public var accessGroup: String?

    public init(service: String = "ro.ird.p2p.identity", accessGroup: String? = nil) {
        self.service = service
        self.accessGroup = accessGroup
    }

    private func query(_ account: String) -> [String: Any] {
        var q: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account]
        if let accessGroup { q[kSecAttrAccessGroup as String] = accessGroup }
        return q
    }

    public func load(account: String) throws -> Identity? {
        var q = query(account)
        q[kSecReturnData as String] = true
        q[kSecMatchLimit as String] = kSecMatchLimitOne
        var item: CFTypeRef?
        let status = SecItemCopyMatching(q as CFDictionary, &item)
        if status == errSecItemNotFound { return nil }
        guard status == errSecSuccess else { throw IdentityStoreError.keychain(status: status) }
        guard let data = item as? Data, let seed = String(data: data, encoding: .utf8) else { throw IdentityStoreError.corrupt }
        do { return try Identity(seedBase64URL: seed) } catch { throw IdentityStoreError.corrupt }
    }

    public func save(_ identity: Identity, account: String) throws {
        let data = Data(identity.seedBase64URL.utf8)
        var add = query(account)
        add[kSecValueData as String] = data
        add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        let status = SecItemAdd(add as CFDictionary, nil)
        if status == errSecDuplicateItem {
            let update = SecItemUpdate(query(account) as CFDictionary, [kSecValueData as String: data] as CFDictionary)
            guard update == errSecSuccess else { throw IdentityStoreError.keychain(status: update) }
            return
        }
        guard status == errSecSuccess else { throw IdentityStoreError.keychain(status: status) }
    }

    public func delete(account: String) throws {
        let status = SecItemDelete(query(account) as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else { throw IdentityStoreError.keychain(status: status) }
    }
}
#endif

public extension IdentityStore {
    /// Loads the identity for `account`, generating and saving one when missing.
    func loadOrCreate(account: String = "default") throws -> Identity {
        if let existing = try load(account: account) { return existing }
        let fresh = Identity.generate()
        try save(fresh, account: account)
        return fresh
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-swift.git