Pular para o conteúdo
Escalibur Wallet
Auditoria

As chaves do Secure Enclave

Kit/Sources/EscaliburKeys/Enclave.swift

Chaves de hardware que embrulham a chave raiz (RK).

467 linhasSHA-256 0a02e1cb633cb410…Abrir no GitHub, no commit 015f5f5
1import CryptoKit
2import EscaliburCore
3import Foundation
4import LocalAuthentication
5import Security
6
7/// Chaves de hardware que embrulham a chave raiz (RK).
8///
9/// Chaves P-256 no Secure Enclave:
10/// - `K_pin` (e `K_pin` da troca de PIN em andamento): `.privateKeyUsage` +
11/// `.applicationPassword`, com a senha derivada do PIN. O SE so desembrulha com ela:
12/// cada chute de PIN passa por este aparelho, um de cada vez (auditoria 3, A1;
13/// conferido num iPhone 13 Pro Max em 10/10/2026: a senha errada e recusada pelo
14/// proprio SE, 22 ms por tentativa).
15/// - `K_dev`: so `.privateKeyUsage`. A chave do PIN das versoes anteriores; e trocada
16/// pela `K_pin` no primeiro PIN certo e apagada.
17/// - `K_bio`: `.privateKeyUsage` + `.biometryCurrentSet`. Embrulha a RK para o
18/// caminho do Face ID. A decisao de liberar e do SEP, nao de um booleano no app, e
19/// cadastrar um rosto novo destroi a chave em vez de abri-la.
20/// - Modo reforcado: `kand.bio` (`.biometryCurrentSet`), `kand.qualquer`
21/// (`.biometryAny`, a unica chave com essa flag, que nunca embrulha a RK, so o miolo
22/// que ainda pede o PIN) e `kand.pin.a`/`kand.pin.b` (`.applicationPassword`). As
23/// flags de cada uma saem de `SecureEnclaveWrapper.flags(for:)`.
24///
25/// Nenhum embrulho abre fora deste aparelho: a parte privada nunca sai do SE.
26public protocol KeyWrapper: Sendable {
27 func createKey(_ slot: WrapSlot) throws
28 func hasKey(_ slot: WrapSlot) -> Bool
29 func deleteKey(_ slot: WrapSlot)
30 /// Embrulha com a chave de um slot sem senha (aparelho e rosto). Nos slots do PIN, use o
31 /// de baixo: buscar a chave sem a senha fazia o iOS pedir a senha na tela.
32 func wrap(_ secret: SecureBytes, slot: WrapSlot) throws -> Data
33 /// Embrulha com a chave de um slot do PIN, buscada com a senha de aplicativo e sem
34 /// interface nenhuma (visto no iPhone, 18/10/2026: ao ligar e desligar o Super Security,
35 /// o iOS abria "Digite a senha para Escalibur").
36 func wrap(_ secret: SecureBytes, slot: WrapSlot, password: Data) throws -> Data
37 /// Abre um embrulho. No slot biometrico, o sistema mostra o Face ID com
38 /// `reason`; a chamada bloqueia a thread ate a decisao, entao nunca no MainActor.
39 func unwrap(_ blob: Data, slot: WrapSlot, reason: String?) throws -> SecureBytes
40 /// Cria a chave do slot do PIN com senha de aplicativo. Substitui a que houver no
41 /// mesmo slot: o slot do PIN so e gravado quando nao existe.
42 func createKey(_ slot: WrapSlot, password: Data) throws
43 /// Desembrulha com a senha de aplicativo. Senha errada: `EnclaveError.wrongPassword`.
44 func unwrap(_ blob: Data, slot: WrapSlot, password: Data) throws -> SecureBytes
45 /// Existe, nao existe, ou o SE e o chaveiro nao conseguiram dizer. "Nao sei" nunca vira
46 /// "nao existe": e o que separa "o Face ID mudou" de "o iPhone esta bloqueado".
47 func probeKey(_ slot: WrapSlot) -> ItemState
48 /// Abre `blob` pelo rosto, com um Face ID so, e na mesma sessao, sem interface nenhuma,
49 /// tenta abrir `watch` com a chave `watchSlot` (a vigia do modo reforcado). A recusa do
50 /// rosto lanca como `unwrap(_:slot:reason:)`; a vigia nunca lanca: diz o que viu.
51 func unwrap(_ blob: Data, slot: WrapSlot, reason: String, watching watch: Data, with watchSlot: WrapSlot) throws -> (SecureBytes, WatchResult)
52}
53
54/// O que a vigia viu na sessao do rosto que acabou de abrir a outra camada.
55public enum WatchResult: Sendable {
56 /// Abriu, sem pedir nada: o conteudo, que quem recebe zera.
57 case opened(SecureBytes)
58 /// A chave da vigia nao existe mais: o SE a destruiu quando o cadastro mudou.
59 case gone
60 /// Precisaria de interface, ou um erro que nao se classifica. Quem decide e o hash do
61 /// cadastro, e na duvida conta como troca.
62 case unknown
63}
64
65public enum WrapSlot: String, Sendable, CaseIterable {
66 case device = "kdev"
67 case biometry = "kbio"
68 /// A chave do slot principal do PIN, com senha de aplicativo.
69 case pin = "kpin"
70 /// A do slot da troca de PIN em andamento.
71 case pinPending = "kpin.novo"
72 /// Modo reforcado: a camada de fora do rk.and que acompanha o cadastro de rostos,
73 /// `.biometryCurrentSet`. A unica de fora em "Bloquear as carteiras"; a vigia em
74 /// "Esperar 24 horas".
75 case hardenedBiometry = "kand.bio"
76 /// Modo reforcado: a camada de fora que sobrevive a um rosto novo, `.biometryAny`. Em
77 /// "Continuar normal" e "Esperar 24 horas". Nunca embrulha a RK: so o miolo, que ainda
78 /// pede o PIN pela `kand.pin`.
79 case hardenedBiometryAny = "kand.qualquer"
80 /// Modo reforcado: a camada de dentro, `.applicationPassword` derivada do PIN. Duas
81 /// geracoes que se alternam na troca de PIN; o cabecalho do rk.and diz qual vale.
82 case hardenedPINA = "kand.pin.a"
83 case hardenedPINB = "kand.pin.b"
84
85 public var usesBiometry: Bool { [.biometry, .hardenedBiometry, .hardenedBiometryAny].contains(self) }
86 /// A chave morre quando o cadastro de rostos muda (`.biometryCurrentSet`).
87 public var followsEnrollment: Bool { self == .biometry || self == .hardenedBiometry }
88 public var usesPassword: Bool { [.pin, .pinPending, .hardenedPINA, .hardenedPINB].contains(self) }
89}
90
91public enum EnclaveError: Error, Equatable, Sendable {
92 case unavailable
93 case keyMissing
94 /// A autenticacao nao liberou a chave desta vez. Se o cadastro de rostos mudou, e
95 /// o RootKeyVault que decide, comparando o estado gravado.
96 case cancelled
97 case failed(String)
98 /// O SE recusou a senha de aplicativo: o PIN nao e este.
99 case wrongPassword
100}
101
102/// O Secure Enclave de verdade.
103public final class SecureEnclaveWrapper: KeyWrapper, @unchecked Sendable {
104 private static let tagPrefix = "app.escalibur.wallet."
105 private static let algorithm = SecKeyAlgorithm.eciesEncryptionCofactorVariableIVX963SHA256AESGCM
106
107 public init() {}
108
109 public static var isAvailable: Bool { SecureEnclave.isAvailable }
110
111 private func tag(_ slot: WrapSlot) -> Data { Data((Self.tagPrefix + slot.rawValue).utf8) }
112
113 public func createKey(_ slot: WrapSlot) throws {
114 // A chave do aparelho nunca e substituida em silencio: ela e o que abre a RK
115 // de todas as carteiras. So a da biometria e recriada ao religar.
116 if slot == .device, hasKey(.device) { throw EnclaveError.failed("chave do aparelho ja existe") }
117 guard slot == .device || slot.usesBiometry else { throw EnclaveError.failed("o slot do PIN pede senha") }
118 deleteKey(slot)
119 var error: Unmanaged<CFError>?
120 guard let access = SecAccessControlCreateWithFlags(nil, KeychainStore.accessibility, Self.flags(for: slot), &error) else {
121 throw EnclaveError.failed("controle de acesso")
122 }
123 let attributes: [String: Any] = [
124 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
125 kSecAttrKeySizeInBits as String: 256,
126 kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
127 kSecPrivateKeyAttrs as String: [
128 kSecAttrIsPermanent as String: true,
129 kSecAttrApplicationTag as String: tag(slot),
130 kSecAttrAccessControl as String: access,
131 ],
132 ]
133 guard SecKeyCreateRandomKey(attributes as CFDictionary, &error) != nil else {
134 throw EnclaveError.unavailable
135 }
136 }
137
138 public func hasKey(_ slot: WrapSlot) -> Bool {
139 let context = LAContext()
140 context.interactionNotAllowed = true
141 return (try? privateKey(slot, context: context)) != nil
142 }
143
144 public func probeKey(_ slot: WrapSlot) -> ItemState {
145 let context = LAContext()
146 context.interactionNotAllowed = true
147 defer { context.invalidate() }
148 let query: [String: Any] = [
149 kSecClass as String: kSecClassKey,
150 kSecAttrApplicationTag as String: tag(slot),
151 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
152 kSecReturnAttributes as String: true,
153 kSecUseAuthenticationContext as String: context,
154 ]
155 switch SecItemCopyMatching(query as CFDictionary, nil) {
156 case errSecSuccess, errSecInteractionNotAllowed, errSecAuthFailed: return .present
157 case errSecItemNotFound: return .absent
158 default: return .unknown
159 }
160 }
161
162 public func deleteKey(_ slot: WrapSlot) {
163 let query: [String: Any] = [
164 kSecClass as String: kSecClassKey,
165 kSecAttrApplicationTag as String: tag(slot),
166 kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
167 ]
168 SecItemDelete(query as CFDictionary)
169 }
170
171 private func privateKey(_ slot: WrapSlot, context: LAContext?) throws -> SecKey {
172 var query: [String: Any] = [
173 kSecClass as String: kSecClassKey,
174 kSecAttrApplicationTag as String: tag(slot),
175 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
176 kSecReturnRef as String: true,
177 ]
178 if let context { query[kSecUseAuthenticationContext as String] = context }
179 var item: CFTypeRef?
180 let status = SecItemCopyMatching(query as CFDictionary, &item)
181 guard status == errSecSuccess, let item else { throw EnclaveError.keyMissing }
182 return item as! SecKey // swiftlint:disable:this force_cast
183 }
184
185 public func wrap(_ secret: SecureBytes, slot: WrapSlot) throws -> Data {
186 // A kand.qualquer so embrulha o miolo, o ECIES da kand.pin, bem maior que a RK: um
187 // segredo do tamanho dela nunca passa por aqui.
188 if slot == .hardenedBiometryAny, secret.count <= 32 { throw EnclaveError.failed("kand.qualquer so embrulha o miolo") }
189 // Nos slots do PIN, so com a senha (o de baixo). Aqui, nenhuma interface do sistema.
190 guard !slot.usesPassword else { throw EnclaveError.failed("o slot do PIN embrulha com a senha") }
191 let context = LAContext()
192 context.interactionNotAllowed = true
193 defer { context.invalidate() }
194 return try encrypt(secret, with: privateKey(slot, context: context))
195 }
196
197 public func wrap(_ secret: SecureBytes, slot: WrapSlot, password: Data) throws -> Data {
198 guard slot.usesPassword else { throw EnclaveError.failed("senha so nos slots do PIN") }
199 let context = LAContext()
200 defer { Self.release(context) }
201 context.interactionNotAllowed = true
202 guard context.setCredential(password, type: .applicationPassword) else { throw EnclaveError.failed("credencial") }
203 return try encrypt(secret, with: privateKey(slot, context: context))
204 }
205
206 private func encrypt(_ secret: SecureBytes, with key: SecKey) throws -> Data {
207 guard let publicKey = SecKeyCopyPublicKey(key) else { throw EnclaveError.keyMissing }
208 var error: Unmanaged<CFError>?
209 let blob = secret.withUnsafeData { plain in
210 SecKeyCreateEncryptedData(publicKey, Self.algorithm, plain as CFData, &error)
211 }
212 guard let blob else { throw EnclaveError.failed("embrulho") }
213 return blob as Data
214 }
215
216 public func unwrap(_ blob: Data, slot: WrapSlot, reason: String?) throws -> SecureBytes {
217 let context = Self.faceContext(reason: reason)
218 defer { context.invalidate() }
219 return try decrypt(blob, slot: slot, context: context)
220 }
221
222 /// Um Face ID, e a vigia no mesmo contexto ja avaliado, agora sem interface: nenhum
223 /// segundo rosto. Se o contexto nao servir para a outra chave, ou ela recusar sem dizer
224 /// por que, a vigia volta "nao se sabe" e quem decide e o hash do cadastro. So a chave
225 /// que o chaveiro diz que nao existe e troca certa (conferir no aparelho, §2.6).
226 public func unwrap(_ blob: Data, slot: WrapSlot, reason: String, watching watch: Data, with watchSlot: WrapSlot) throws -> (SecureBytes, WatchResult) {
227 let context = Self.faceContext(reason: reason)
228 defer { context.invalidate() }
229 let opened = try decrypt(blob, slot: slot, context: context)
230 context.interactionNotAllowed = true
231 let query: [String: Any] = [
232 kSecClass as String: kSecClassKey,
233 kSecAttrApplicationTag as String: tag(watchSlot),
234 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
235 kSecReturnRef as String: true,
236 kSecUseAuthenticationContext as String: context,
237 ]
238 var item: CFTypeRef?
239 let status = SecItemCopyMatching(query as CFDictionary, &item)
240 if status == errSecItemNotFound { return (opened, .gone) }
241 guard status == errSecSuccess, let item else { return (opened, .unknown) }
242 let key = item as! SecKey // swiftlint:disable:this force_cast
243 var error: Unmanaged<CFError>?
244 guard let plain = SecKeyCreateDecryptedData(key, Self.algorithm, watch as CFData, &error) else {
245 _ = error?.takeRetainedValue()
246 return (opened, .unknown)
247 }
248 return (opened, .opened(SecureBytes.consuming(plain)))
249 }
250
251 /// O contexto de um Face ID. Nenhum reuso de autenticacao: cada assinatura pede presenca
252 /// nova. Sem botao de alternativa do sistema: a alternativa e o PIN do app. Sem motivo,
253 /// nenhuma interface.
254 private static func faceContext(reason: String?) -> LAContext {
255 let context = LAContext()
256 context.touchIDAuthenticationAllowableReuseDuration = 0
257 context.localizedFallbackTitle = ""
258 if let reason { context.localizedReason = reason } else { context.interactionNotAllowed = true }
259 return context
260 }
261
262 private func decrypt(_ blob: Data, slot: WrapSlot, context: LAContext) throws -> SecureBytes {
263 let key: SecKey
264 do {
265 key = try privateKey(slot, context: context)
266 } catch {
267 throw EnclaveError.keyMissing
268 }
269 var error: Unmanaged<CFError>?
270 guard let plain = SecKeyCreateDecryptedData(key, Self.algorithm, blob as CFData, &error) else {
271 let nsError = (error?.takeRetainedValue() as Error?).map { $0 as NSError }
272 // Qualquer recusa da autenticacao (cancelar, ligacao chegando, bloqueio por
273 // tentativas, rosto nao reconhecido) e "nao desta vez", nao "a biometria
274 // mudou". Quem decide se o cadastro mudou e o RootKeyVault, pelo estado.
275 if nsError?.domain == LAErrorDomain || nsError?.code == Int(errSecUserCanceled) || nsError?.code == Int(errSecAuthFailed) {
276 throw EnclaveError.cancelled
277 }
278 throw EnclaveError.failed("desembrulho")
279 }
280 return SecureBytes.consuming(plain)
281 }
282}
283
284extension SecureEnclaveWrapper {
285 /// As flags do controle de acesso de cada chave. A unica com `.biometryAny` e a
286 /// `kand.qualquer`, a camada de fora do modo reforcado que sobrevive a um rosto novo:
287 /// sozinha ela nao abre nada (por baixo, a `kand.pin` pede o PIN), e o `verificar.sh`
288 /// confere que a excecao nao sai desta linha.
289 static func flags(for slot: WrapSlot) -> SecAccessControlCreateFlags {
290 switch slot {
291 case .device: return [.privateKeyUsage]
292 case .biometry, .hardenedBiometry: return [.privateKeyUsage, .biometryCurrentSet]
293 case .hardenedBiometryAny: return [.privateKeyUsage, .biometryAny]
294 case .pin, .pinPending, .hardenedPINA, .hardenedPINB: return [.privateKeyUsage, .applicationPassword]
295 }
296 }
297
298 /// O contexto com a senha de aplicativo vale para uma chamada so: na volta, a
299 /// credencial e solta e o contexto invalidado, para a senha derivada do PIN nao ficar
300 /// retida num objeto vivo (auditoria 4, C7-21). O mesmo que `KeychainStore` faz.
301 private static func release(_ context: LAContext) {
302 context.setCredential(nil, type: .applicationPassword)
303 context.invalidate()
304 }
305
306 public func createKey(_ slot: WrapSlot, password: Data) throws {
307 guard slot.usesPassword else { throw EnclaveError.failed("senha so nos slots do PIN") }
308 deleteKey(slot)
309 let context = LAContext()
310 defer { Self.release(context) }
311 guard context.setCredential(password, type: .applicationPassword) else { throw EnclaveError.failed("credencial") }
312 var error: Unmanaged<CFError>?
313 guard let access = SecAccessControlCreateWithFlags(nil, KeychainStore.accessibility, Self.flags(for: slot), &error) else {
314 throw EnclaveError.failed("controle de acesso")
315 }
316 let attributes: [String: Any] = [
317 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
318 kSecAttrKeySizeInBits as String: 256,
319 kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
320 kSecUseAuthenticationContext as String: context,
321 kSecPrivateKeyAttrs as String: [
322 kSecAttrIsPermanent as String: true,
323 kSecAttrApplicationTag as String: Data(("app.escalibur.wallet." + slot.rawValue).utf8),
324 kSecAttrAccessControl as String: access,
325 ],
326 ]
327 guard SecKeyCreateRandomKey(attributes as CFDictionary, &error) != nil else { throw EnclaveError.unavailable }
328 }
329
330 public func unwrap(_ blob: Data, slot: WrapSlot, password: Data) throws -> SecureBytes {
331 let context = LAContext()
332 defer { Self.release(context) }
333 context.interactionNotAllowed = true
334 guard context.setCredential(password, type: .applicationPassword) else { throw EnclaveError.failed("credencial") }
335 let query: [String: Any] = [
336 kSecClass as String: kSecClassKey,
337 kSecAttrApplicationTag as String: Data(("app.escalibur.wallet." + slot.rawValue).utf8),
338 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
339 kSecReturnRef as String: true,
340 kSecUseAuthenticationContext as String: context,
341 ]
342 var item: CFTypeRef?
343 guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, let item else { throw EnclaveError.keyMissing }
344 let key = item as! SecKey // swiftlint:disable:this force_cast
345 var error: Unmanaged<CFError>?
346 guard let plain = SecKeyCreateDecryptedData(key, .eciesEncryptionCofactorVariableIVX963SHA256AESGCM, blob as CFData, &error) else {
347 let nsError = (error?.takeRetainedValue() as Error?).map { $0 as NSError }
348 // A senha errada volta do proprio SE como erro do CryptoTokenKit (no iPhone,
349 // "dado corrompido": a senha entra na chave) ou como falha de autenticacao.
350 if nsError?.domain == "CryptoTokenKit" || nsError?.code == Int(errSecAuthFailed) || nsError?.domain == LAErrorDomain {
351 throw EnclaveError.wrongPassword
352 }
353 throw EnclaveError.failed("desembrulho")
354 }
355 return SecureBytes.consuming(plain)
356 }
357}
358
359extension SecureBytes {
360 /// Copia de um `CFData` que acabou de sair de uma API do sistema para o buffer
361 /// seguro, e apaga a origem. O `CFData` e imutavel pela API, mas a memoria e do
362 /// processo: zerar e o que encurta a janela do segredo em claro.
363 static func consuming(_ data: CFData) -> SecureBytes {
364 let length = CFDataGetLength(data)
365 let out = SecureBytes(capacity: max(length, 1))
366 if let pointer = CFDataGetBytePtr(data), length > 0 {
367 out.append(contentsOf: UnsafeBufferPointer(start: pointer, count: length))
368 memset_s(UnsafeMutableRawPointer(mutating: pointer), length, 0, length)
369 }
370 return out
371 }
372}
373
374#if DEBUG
375/// So no build de teste: o experimento do MUST 1 (auditoria 1) e do A1 (auditoria 3).
376/// Uma chave descartavel no Secure Enclave com `.privateKeyUsage` e
377/// `.applicationPassword`, que nunca toca as chaves da carteira (tag propria, apagada no
378/// fim). Confere no aparelho de verdade se o SE abre com a senha certa e recusa a
379/// errada e a falta dela, sem pedir nada na tela, e quanto custa cada tentativa.
380public enum EnclaveSpike {
381 static let tag = Data("app.escalibur.wallet.experimento".utf8)
382 static let algorithm = SecKeyAlgorithm.eciesEncryptionCofactorVariableIVX963SHA256AESGCM
383
384 public static func run() -> [String] {
385 var log: [String] = ["Secure Enclave disponivel: \(SecureEnclave.isAvailable)"]
386 delete()
387 defer { delete() }
388 let password = Data("senha-certa-do-experimento".utf8)
389 let creation = LAContext()
390 creation.setCredential(password, type: .applicationPassword)
391 var error: Unmanaged<CFError>?
392 guard let access = SecAccessControlCreateWithFlags(nil, KeychainStore.accessibility, [.privateKeyUsage, .applicationPassword], &error) else {
393 log.append("controle de acesso: recusado (\(describe(error)))")
394 return log
395 }
396 let attributes: [String: Any] = [
397 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
398 kSecAttrKeySizeInBits as String: 256,
399 kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
400 kSecUseAuthenticationContext as String: creation,
401 kSecPrivateKeyAttrs as String: [
402 kSecAttrIsPermanent as String: true,
403 kSecAttrApplicationTag as String: tag,
404 kSecAttrAccessControl as String: access,
405 ],
406 ]
407 guard let created = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
408 log.append("criar a chave: recusado (\(describe(error)))")
409 return log
410 }
411 log.append("criar a chave: ok")
412 guard let publicKey = SecKeyCopyPublicKey(created),
413 let blob = SecKeyCreateEncryptedData(publicKey, algorithm, Data("segredo do experimento".utf8) as CFData, &error) as Data?
414 else {
415 log.append("embrulhar: falhou (\(describe(error)))")
416 return log
417 }
418 log.append(attempt("senha certa", credential: password))
419 log.append(attempt("senha errada", credential: Data("senha-errada".utf8)))
420 log.append(attempt("sem senha", credential: nil))
421 let start = Date()
422 var refused = 0
423 for index in 0..<20 where !attempt("errada \(index)", credential: Data("errada-\(index)".utf8)).hasSuffix("abriu") { refused += 1 }
424 log.append("20 erradas seguidas: \(refused) recusadas, \(String(format: "%.0f", Date().timeIntervalSince(start) * 1000 / 20)) ms cada")
425 log.append(attempt("senha certa depois das 20 erradas", credential: password))
426 _ = blob
427 return log
428
429 func attempt(_ label: String, credential: Data?) -> String {
430 let context = LAContext()
431 context.interactionNotAllowed = true
432 if let credential { context.setCredential(credential, type: .applicationPassword) }
433 let query: [String: Any] = [
434 kSecClass as String: kSecClassKey,
435 kSecAttrApplicationTag as String: tag,
436 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
437 kSecReturnRef as String: true,
438 kSecUseAuthenticationContext as String: context,
439 ]
440 var item: CFTypeRef?
441 let status = SecItemCopyMatching(query as CFDictionary, &item)
442 guard status == errSecSuccess, let item else { return "\(label): chave nao encontrada (\(status))" }
443 let key = item as! SecKey // swiftlint:disable:this force_cast
444 var failure: Unmanaged<CFError>?
445 if let plain = SecKeyCreateDecryptedData(key, algorithm, blob as CFData, &failure) as Data? {
446 return "\(label): \(plain == Data("segredo do experimento".utf8) ? "abriu" : "abriu com conteudo errado")"
447 }
448 return "\(label): recusou (\(describe(failure)))"
449 }
450 }
451
452 static func delete() {
453 let query: [String: Any] = [
454 kSecClass as String: kSecClassKey,
455 kSecAttrApplicationTag as String: tag,
456 kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
457 ]
458 SecItemDelete(query as CFDictionary)
459 }
460
461 static func describe(_ error: Unmanaged<CFError>?) -> String {
462 guard let error = error?.takeRetainedValue() else { return "sem erro" }
463 let ns = error as Error as NSError
464 return "\(ns.domain) \(ns.code)"
465 }
466}
467#endif