Pular para o conteúdo
Escalibur Wallet
Auditoria

A chave raiz, o PIN e o Face ID

Kit/Sources/EscaliburKeys/RootKeyVault.swift

A chave raiz (RK) e os dois caminhos que a destrancam: PIN e biometria.

1.907 linhasSHA-256 1cfc884227c4108d…Abrir no GitHub, no commit 015f5f5
1import CryptoKit
2import EscaliburCore
3import Foundation
4import LocalAuthentication
5
6/// A chave raiz (RK) e os dois caminhos que a destrancam: PIN e biometria.
7///
8/// ```
9/// PIN ─Argon2id─► AP ─┬─(senha de aplicativo do item)─► item rk.pin
10/// ├─HKDF─► senha da chave do PIN no SE
11/// └─HKDF─► K_pin
12/// item rk.pin = embrulho(chave do PIN no SE, ChaChaPoly(K_pin, RK)) ─SE+senha─► ─K_pin─► RK
13/// Face ID (decisao no SEP) ─► item rk.bio = embrulho(K_bio, RK) ─SE─► RK
14/// ```
15///
16/// Formato 2 do slot (auditoria 3, A1): a chave do SE que embrulha o slot tem senha de
17/// aplicativo derivada do PIN, entao o SE so desembrulha com o PIN certo. Quem tirar uma
18/// copia do slot nao consegue mais testar PINs fora do aparelho depois de uma unica
19/// chamada ao SE: cada chute passa pelo SE deste iPhone, mais um Argon2id. O formato 1
20/// (chave do aparelho sem senha) migra no primeiro PIN certo.
21///
22/// **Nao existe verificador de PIN gravado em lugar nenhum.** O PIN errado e recusado
23/// pelo chaveiro, que nao devolve o item sem a senha de aplicativo certa, e de novo
24/// pela camada interna, que so abre com a chave derivada do PIN. A camada interna e o
25/// que garante o desenho mesmo se a senha de aplicativo for so uma regra de acesso e
26/// nao entrar na chave do item: quem copiar o item e conseguir usar o SE deste aparelho
27/// ainda precisa do PIN, e cada chute custa um Argon2id de 256 MiB
28/// (docs/seguranca.md §2).
29public final class RootKeyVault: @unchecked Sendable {
30 public enum Failure: Error, Equatable, Sendable {
31 case notSetUp
32 case alreadySetUp
33 case wrongPIN(remainingBeforeWipe: UInt32?)
34 case throttled(seconds: TimeInterval)
35 /// Nao sao 4 nem 6 digitos de 0 a 9.
36 case malformedPIN
37 case biometryNotEnabled
38 /// O cadastro de rostos mudou desde que o Face ID foi ligado: o slot foi
39 /// apagado e religar exige o PIN.
40 case biometryChanged
41 /// O Face ID nao abriu desta vez (cancelou, nao reconheceu, bloqueio do
42 /// sistema). O slot continua; o caminho agora e o PIN.
43 case cancelled
44 /// Apagar apos erros disparou: tudo foi destruido.
45 case wiped
46 /// O iPhone reiniciou (ou a bateria acabou) desde o ultimo PIN: o Face ID so volta
47 /// a valer depois do PIN digitado, como no proprio iPhone.
48 case pinRequiredAfterRestart
49 /// O Face ID caiu ou foi religado ha pouco: ver as palavras, lacrar o envelope e
50 /// remover carteira esperam `seconds` (`PINPolicy.biometryLossHold`). No modo
51 /// reforcado com "Esperar 24 horas", o cadastro de rostos mudou: tudo menos abrir o
52 /// app espera.
53 case biometryHold(seconds: TimeInterval)
54 /// O Face ID caiu sem o dono pedir e ainda nao foi religado: o que pede PIN e rosto
55 /// fica recusado ate religar (e a espera conta a partir dai). Nunca volta sozinho a
56 /// pedir so o PIN.
57 case biometryRequired
58 /// O Face ID nao esta disponivel agora (tirado do app, Espelhamento do iPhone, falha
59 /// passageira do sistema). Nada e apagado; o que pede PIN e rosto recusa.
60 case biometryUnavailable
61 case storage
62 /// Modo reforcado ligado: o caminho e `unlockHardened`, nunca o PIN so nem o rosto so.
63 case hardenedModeActive
64 case notHardened
65 /// Modo reforcado: o rosto nao confirmou. Contou como tentativa.
66 case biometryNotConfirmed(remainingBeforeWipe: UInt32?)
67 /// O Face ID esta bloqueado pelo sistema (5 erros): nada foi tentado nem contado.
68 case biometryLockedOut
69 /// Modo reforcado sem a trava: nenhum rosto cadastrado no Face ID agora. Nada foi
70 /// tentado, contado nem apagado; volta a abrir com um rosto cadastrado.
71 case biometryNotEnrolled
72 /// Modo reforcado: a chave do rosto que a politica usa sumiu (com a trava, o cadastro
73 /// de rostos mudou) e o rk.and nao abre mais. Nada foi apagado; o caminho e restaurar
74 /// pelas palavras.
75 case hardenedSlotLost
76 /// Modo reforcado: a chave do rosto existe e o SE recusou abrir a camada de fora por um
77 /// erro que nao vem do Face ID (uma redefinicao do Face ID interrompida, por exemplo).
78 /// Nao conta tentativa e nao apaga nada.
79 case hardenedFaceKeyFailed
80 case hardenedBlocked(HardenedBlocker)
81 }
82
83 /// O que impede ligar o modo reforcado agora.
84 public enum HardenedBlocker: Sendable, Equatable { case biometryOff, changePending, legacySlot, biometryUnavailable }
85
86 enum Account {
87 static let pinKDF = "pin.kdf"
88 /// Quantos digitos tem o PIN do slot (1 byte), para a tela saber quando o PIN
89 /// acabou sem gastar tentativa. Sem o item, 6: o tamanho de antes da escolha.
90 static let pinLength = "pin.digitos"
91 static let rootByPIN = "rk.pin"
92 static let rootByBiometry = "rk.bio"
93 static let biometryState = "bio.estado"
94 /// O prazo da espera depois que o Face ID caiu ou foi religado (`HoldRecord`).
95 static let biometryHold = "bio.espera"
96 /// O Face ID caiu sem o dono pedir e ainda nao foi religado (1 byte). Sem prazo: so
97 /// um `enableBiometry` que confere o rosto tira a marca.
98 static let biometryLost = "bio.perdido"
99 static let attempts = "pin.tentativas"
100 static let wipeAfterErrors = "pin.apagar"
101 static let pinBoot = "pin.boot"
102 static let pendingSuffix = ".novo"
103 /// Modo reforcado: o slot unico, PIN e rosto em camadas (docs/seguranca.md §2.6).
104 static let rootHardened = "rk.and"
105 /// O hash do cadastro de rostos que o modo reforcado conhece: o de quando a `kand.bio`
106 /// nasceu (ou o de quando a politica passou a valer, na `.continue`). Na `.wait`,
107 /// decide quando a vigia nao responde; nas outras, so classifica o erro.
108 static let hardenedState = "and.estado"
109 /// A espera do `.wait` depois que a vigia viu outro cadastro (`HardenedHold`).
110 static let hardenedHold = "and.espera"
111 }
112
113 /// O registro de custo do slot: sal (16), memoria (4), passadas (4), faixas (1) e, no
114 /// formato 2, a versao (1).
115 enum SlotFormat {
116 static let legacyLength = 25
117 static let version: UInt8 = 2
118 static let length = 26
119 static let passwordInfo = Data("escalibur-wallet/v2/kpin.senha".utf8)
120 }
121
122 /// A chave do SE de cada slot do PIN no formato 2.
123 static func wrapSlot(_ suffix: String) -> WrapSlot { suffix.isEmpty ? .pin : .pinPending }
124
125 /// A senha de aplicativo da chave do PIN no SE: derivada do AP, separada da chave da
126 /// camada interna pelo `info`.
127 ///
128 /// Sai em buffer seguro, que quem chama zera; o `Data` que o SE pede so existe dentro
129 /// de `withUnsafeData`, zerado na volta. Antes era um `Data` que ninguem zerava, e a
130 /// senha do PIN no SE ficava no heap depois do uso (auditoria 4, C7-21).
131 static func devicePassword(ap: SecureBytes, salt: [UInt8]) -> SecureBytes {
132 let material = ap.withUnsafeBytes { SymmetricKey(data: $0) }
133 return secure(HKDF<SHA256>.deriveKey(inputKeyMaterial: material, salt: salt, info: SlotFormat.passwordInfo, outputByteCount: 32))
134 }
135
136 /// Os bytes de uma chave do CryptoKit num buffer seguro.
137 static func secure(_ key: SymmetricKey) -> SecureBytes {
138 let out = SecureBytes(capacity: key.bitCount / 8)
139 key.withUnsafeBytes { out.append(contentsOf: $0.bindMemory(to: UInt8.self)) }
140 return out
141 }
142
143 /// Camada interna do slot do PIN: versao, nonce (12), RK cifrada (32), tag (16).
144 enum Inner {
145 static let version: UInt8 = 1
146 static let length = 1 + 12 + 32 + 16
147 static let info = Data("escalibur-wallet/v1/rk.pin".utf8)
148 }
149
150 let store: any SecretStore
151 let wrapper: any KeyWrapper
152 /// O Face ID deste iPhone agora. Trocavel so nos testes, que rodam num Mac sem ele.
153 var biometryAvailability: @Sendable () -> BiometryAvailability = { RootKeyVault.systemBiometryAvailability() }
154 /// O Face ID para o modo reforcado e o hash do cadastro. Trocaveis so nos testes.
155 var hardenedBiometryStatus: @Sendable () -> BiometryStatus = { RootKeyVault.systemBiometryStatus() }
156 var hardenedDomainState: @Sendable () -> Data? = { RootKeyVault.biometryDomainState() }
157 /// Custo fixo do PIN, so para os testes nao pagarem 256 MiB por chamada.
158 private let fixedPINParameters: KDFParameters?
159 private let lock = NSLock()
160
161 public convenience init(store: any SecretStore, wrapper: any KeyWrapper) {
162 self.init(store: store, wrapper: wrapper, pinParameters: nil)
163 }
164
165 init(store: any SecretStore, wrapper: any KeyWrapper, pinParameters: KDFParameters?) {
166 self.store = store
167 self.wrapper = wrapper
168 self.fixedPINParameters = pinParameters
169 }
170
171 private var pinParameters: KDFParameters { fixedPINParameters ?? Self.calibratedPINParameters() }
172
173 /// Existe algum slot do PIN? Quando o chaveiro nao consegue responder, a resposta
174 /// e "sim": mandar o dono para o cadastro por causa de um erro de leitura e o
175 /// caminho que um dia apagaria a chave do aparelho. O cadastro, por sua vez, so
176 /// prossegue com um "nao existe" definitivo.
177 public var isSetUp: Bool {
178 let slots = [Account.rootByPIN, Account.rootByPIN + Account.pendingSuffix, Account.rootHardened].map(store.probe)
179 return slots.contains(.present) || slots.contains(.unknown)
180 }
181
182 // MARK: Cadastro
183
184 /// Primeiro PIN: cria K_dev, sorteia a RK, grava o slot do PIN e confere lendo de
185 /// volta. Devolve a RK aberta, para o chamador derivar a chave de indice e criar a
186 /// primeira carteira sem pedir o PIN de novo; quem recebe zera.
187 public func setUp(pin: SecureBytes) throws -> SecureBytes {
188 lock.lock(); defer { lock.unlock() }
189 let slots = [Account.rootByPIN, Account.rootByPIN + Account.pendingSuffix, Account.rootByBiometry, Account.rootHardened].map(store.probe)
190 guard !slots.contains(.unknown) else { throw Failure.storage }
191 guard !slots.contains(.present) else { throw Failure.alreadySetUp }
192 guard Self.isWellFormed(pin) else { throw Failure.malformedPIN }
193
194 // Sem nenhum slot da RK, o que sobrou de um cadastro interrompido nao abre
195 // nada: a K_dev orfa, o contador e as carteiras cifradas sob uma RK perdida.
196 // Apagar aqui e explicito; `createKey` nunca substitui a K_dev sozinho.
197 try discardOrphans()
198
199 let rk = try SecureBytes.random(count: 32)
200 do {
201 try store.add(AttemptRecord.zero.encoded, account: Account.attempts, protection: .standard, context: nil)
202 try writePINSlot(rk: rk, pin: pin, kdf: pinParameters, suffix: "")
203 try confirm(pin: pin, suffix: "", matches: rk)
204 recordPINThisBoot()
205 } catch {
206 rk.wipe()
207 try? discardOrphans()
208 throw error
209 }
210 return rk
211 }
212
213 private func discardOrphans() throws {
214 WrapSlot.allCases.forEach(wrapper.deleteKey)
215 do { try store.deleteAll() } catch { throw Failure.storage }
216 }
217
218 // MARK: Destrancar
219
220 /// Destranca pelo PIN. O incremento de tentativa e gravado **antes** da avaliacao;
221 /// se a gravacao falhar, a tentativa nao e avaliada (falha fechada).
222 ///
223 /// Com uma troca de PIN interrompida existem dois slots, e os dois sao tentados na
224 /// mesma tentativa. O que abrir decide o desfecho: o antigo desfaz a troca, o novo
225 /// a conclui.
226 public func unlock(pin: SecureBytes) throws -> SecureBytes {
227 lock.lock(); defer { lock.unlock() }
228 return try unlockLocked(pin: pin)
229 }
230
231 private func unlockLocked(pin: SecureBytes) throws -> SecureBytes {
232 switch securityMode {
233 case .hardened: throw Failure.hardenedModeActive
234 case .unknown: throw Failure.storage
235 case .normal: break
236 }
237 let pendingAccount = Account.rootByPIN + Account.pendingSuffix
238 let main = store.probe(Account.rootByPIN)
239 let pending = store.probe(pendingAccount)
240 guard main != .unknown, pending != .unknown else { throw Failure.storage }
241 guard main == .present || pending == .present else { throw Failure.notSetUp }
242
243 // PIN sem forma (envio duplo com o buffer vazio) nunca gasta tentativa.
244 guard Self.isWellFormed(pin) else { throw Failure.malformedPIN }
245 let record = try rebasedAttemptsLocked()
246 let waiting = record.remaining()
247 guard waiting <= 0 else { throw Failure.throttled(seconds: waiting) }
248
249 let next = AttemptRecord.after(failures: record.failures + 1)
250 do { try store.update(next.encoded, account: Account.attempts) } catch { throw Failure.storage }
251
252 let candidates = [main == .present ? "" : nil, pending == .present ? Account.pendingSuffix : nil].compactMap { $0 }
253 var opened: (rk: SecureBytes, suffix: String)?
254 for suffix in candidates where opened == nil {
255 do {
256 opened = (try readPINSlot(pin: pin, suffix: suffix), suffix)
257 } catch Failure.wrongPIN {
258 continue
259 }
260 }
261
262 guard let opened else {
263 if let threshold = wipeThreshold, next.failures >= threshold {
264 wipeAllLocked()
265 throw Failure.wiped
266 }
267 let left = wipeThreshold.map { $0 - next.failures }
268 throw Failure.wrongPIN(remainingBeforeWipe: left)
269 }
270
271 // Contador de volta a zero. Se falhar, o dono so espera mais na proxima vez.
272 try? store.update(AttemptRecord.zero.encoded, account: Account.attempts)
273 recordPINThisBoot()
274 // Um rk.and ao lado do rk.pin e uma transicao que nao chegou ao fim (ligar
275 // interrompido antes do commit, ou desligar interrompido depois do rk.pin gravado).
276 // O PIN acabou de provar o slot do modo normal: o rk.and sai.
277 if store.probe(Account.rootHardened) == .present { discardHardenedLocked() }
278 if pending == .present {
279 if opened.suffix.isEmpty {
280 deleteSlot(suffix: Account.pendingSuffix)
281 } else {
282 try? promotePendingLocked(rk: opened.rk, pin: pin)
283 }
284 }
285 // Slot no formato 1: passa para o 2 agora que o PIN certo esta na mao. Falha aqui
286 // nao tranca ninguem: antes do ponto sem volta o slot antigo continua o unico.
287 if slotVersion(suffix: "") == 1 {
288 try? migrateLocked(rk: opened.rk, pin: pin)
289 } else {
290 dropLegacyKeyIfDone()
291 }
292 return opened.rk
293 }
294
295 /// Com o principal no formato 2 e nenhuma troca pendente, a chave do aparelho antiga
296 /// nao abre mais nada: sai.
297 private func dropLegacyKeyIfDone() {
298 guard slotVersion(suffix: "") == 2, store.probe(Account.rootByPIN + Account.pendingSuffix) == .absent,
299 wrapper.hasKey(.device) else { return }
300 wrapper.deleteKey(.device)
301 }
302
303 /// O formato do slot principal do PIN (1 ou 2), para a tela e os testes. So le.
304 public var pinSlotFormat: Int? { slotVersion(suffix: "") }
305
306 /// Quantos digitos pode ter o PIN que abre: o tamanho de cada slot que existe. Quase
307 /// sempre um so; dois quando uma troca entre tamanhos diferentes foi interrompida antes
308 /// do ponto sem volta (o PIN antigo desfaz, o novo conclui, e a tela aceita os dois).
309 /// Sem o registro, 6 (slot de antes da escolha). Chaveiro mudo ou registro estranho:
310 /// os dois tamanhos, nunca um chute, para a tela nao gastar tentativa fechando no
311 /// tamanho errado. Nao e segredo (o iPhone tambem mostra quantos pontos o codigo tem).
312 /// So le.
313 public var pinLengths: Set<Int> {
314 switch securityMode {
315 case .unknown: return Set(PINPolicy.lengths)
316 case .hardened:
317 guard let record = try? readHardened() else { return Set(PINPolicy.lengths) }
318 return [Int(record.header.pinLength)]
319 case .normal: break
320 }
321 var lengths = Set<Int>()
322 for suffix in ["", Account.pendingSuffix] {
323 switch store.probe(Account.rootByPIN + suffix) {
324 case .absent: continue
325 case .unknown: return Set(PINPolicy.lengths)
326 case .present:
327 guard let length = slotLength(suffix: suffix) else { return Set(PINPolicy.lengths) }
328 lengths.insert(length)
329 }
330 }
331 return lengths.isEmpty ? [PINPolicy.digits] : lengths
332 }
333
334 private func slotLength(suffix: String) -> Int? {
335 switch store.probe(Account.pinLength + suffix) {
336 case .absent: return PINPolicy.digits
337 case .unknown: return nil
338 case .present:
339 guard let data = try? store.read(Account.pinLength + suffix, context: nil), data.count == 1,
340 PINPolicy.lengths.contains(Int(data[0])) else { return nil }
341 return Int(data[0])
342 }
343 }
344
345 /// O formato do slot: 1 (chave do aparelho sem senha), 2 (chave do PIN com senha) ou
346 /// nil (sem slot, registro ilegivel).
347 func slotVersion(suffix: String) -> Int? {
348 guard let record = try? store.read(Account.pinKDF + suffix, context: nil) else { return nil }
349 if record.count == SlotFormat.legacyLength { return 1 }
350 if record.count == SlotFormat.length, record.last == SlotFormat.version { return 2 }
351 return nil
352 }
353
354 /// Formato 1 para 2 com o mesmo PIN, pelo mesmo caminho atomico da troca de PIN: o
355 /// slot novo e gravado e conferido como pendente, o antigo so sai depois, e a chave
356 /// do aparelho antiga so e apagada com o principal novo conferido.
357 private func migrateLocked(rk: SecureBytes, pin: SecureBytes) throws {
358 try replacePINLocked(rk: rk, newPIN: pin)
359 dropLegacyKeyIfDone()
360 }
361
362 /// Destranca pelo Face ID. Bloqueia a thread enquanto o sistema pergunta: chamar
363 /// fora do MainActor. O que autoriza e o SEP liberar a chave, nunca um booleano.
364 public func unlockWithBiometry(reason: String) throws -> SecureBytes {
365 lock.lock(); defer { lock.unlock() }
366 return try unlockWithBiometryLocked(reason: reason)
367 }
368
369 /// Sob a trava: uma varredura do modo reforcado rodando ao lado de uma troca de PIN
370 /// apagaria a geracao nova da chave do PIN (revisao do modo reforcado, M2). No
371 /// reforcado so recusa; quem varre e `rebaseAttempts` e `unlockHardened`, sob a trava.
372 private func unlockWithBiometryLocked(reason: String) throws -> SecureBytes {
373 switch securityMode {
374 case .hardened: throw Failure.hardenedModeActive
375 case .unknown: throw Failure.storage
376 case .normal: break
377 }
378 guard pinEnteredThisBoot else { throw Failure.pinRequiredAfterRestart }
379 let blob: Data?
380 do { blob = try store.read(Account.rootByBiometry, context: nil) } catch { throw Failure.storage }
381 guard let blob else { throw Failure.biometryNotEnabled }
382 guard wrapper.hasKey(.biometry) else {
383 try biometryLost()
384 throw Failure.biometryChanged
385 }
386 let rk: SecureBytes
387 do {
388 rk = try wrapper.unwrap(blob, slot: .biometry, reason: reason)
389 } catch {
390 // So um cadastro de rostos diferente do gravado ao ligar, ou a chave que o
391 // SEP ja destruiu, apagam o slot. Cancelar, nao reconhecer ou o bloqueio
392 // temporario do sistema deixam tudo como esta.
393 if biometryEnrollmentChanged || !wrapper.hasKey(.biometry) {
394 try biometryLost()
395 throw Failure.biometryChanged
396 }
397 throw Failure.cancelled
398 }
399 // O contador de PIN nao zera aqui: quem apresenta o rosto do dono a forca nao
400 // ganha chutes de PIN de graca a cada Face ID (auditoria 2, B4). So o PIN certo zera.
401 // A ancora do boot acompanha a oscilacao do kern.boottime: ainda e o mesmo boot
402 // (conferido acima), so com o instante lido agora.
403 recordPINThisBoot()
404 return rk
405 }
406
407 /// A1: o que entrega ou destroi a carteira inteira (ver as palavras, lacrar o
408 /// envelope, remover, desligar o Face ID) pede o PIN e, com o Face ID ligado, o rosto
409 /// tambem. Quem viu o PIN e pegou o iPhone nao leva a frase; quem apresenta o rosto do
410 /// dono a forca tambem nao. Sem o Face ID ligado, so o PIN.
411 ///
412 /// O PIN vem primeiro e conta tentativa como em `unlock`; o rosto e decidido no SEP,
413 /// pelo slot da biometria, e as duas chaves raiz tem de ser a mesma. Nenhum dos dois
414 /// caminhos cai para o codigo do iPhone.
415 ///
416 /// O Face ID que cai sem o dono pedir (cadastro de rostos mudou, Face ID tirado do app
417 /// ou do iPhone) apaga o slot e comeca a espera de `PINPolicy.biometryLossHold`:
418 /// durante ela, isto recusa antes de pedir qualquer coisa, mesmo com um Face ID
419 /// religado. Sem a espera, o ladrao que viu o codigo do iPhone cadastrava o proprio
420 /// rosto e passava com o PIN so. Chaveiro mudo recusa: nunca vira "Face ID desligado".
421 public func unlockWithPINAndBiometry(pin: SecureBytes, reason: String) throws -> SecureBytes {
422 lock.lock(); defer { lock.unlock() }
423 return try unlockWithPINAndBiometryLocked(pin: pin, reason: reason)
424 }
425
426 private func unlockWithPINAndBiometryLocked(pin: SecureBytes, reason: String) throws -> SecureBytes {
427 try checkPINAndBiometryAllowed()
428 let slot = store.probe(Account.rootByBiometry)
429 guard slot != .unknown else { throw Failure.storage }
430
431 let rk = try unlockLocked(pin: pin)
432 guard slot == .present else { return rk }
433 do {
434 // Face ID indisponivel agora nao e Face ID perdido: nada e apagado, e isto recusa
435 // (revisao do A1: uma falha passageira, ou o Espelhamento, derrubava o slot).
436 if biometryAvailability() == .gone { throw Failure.biometryUnavailable }
437 let check = try unlockWithBiometryLocked(reason: reason)
438 defer { check.wipe() }
439 guard Hash.constantTimeEqual(check, rk) else { throw Failure.storage }
440 return rk
441 } catch {
442 rk.wipe()
443 throw error
444 }
445 }
446
447 /// Quanto falta da espera do A1, para a tela avisar antes de pedir o PIN. Chaveiro mudo
448 /// ou registro ilegivel: a espera cheia, nunca zero. So le.
449 public var biometryHoldRemaining: TimeInterval {
450 (try? biometryHoldRemainingChecked()) ?? PINPolicy.biometryLossHold
451 }
452
453 /// O que pede PIN e rosto pode ser tentado agora? Recusa na espera, e com o Face ID
454 /// perdido e nao religado. Para a tela perguntar antes do PIN e da voz; o cofre confere
455 /// de novo em `unlockWithPINAndBiometry`.
456 public func checkPINAndBiometryAllowed() throws {
457 // No reforcado nao existe slot do rosto do modo normal: a espera e a marca do A1
458 // nao valem (e a varredura as apaga). Vale a espera do `.wait`.
459 guard securityMode != .hardened else {
460 try checkHardenedAllowed()
461 return
462 }
463 let hold = try biometryHoldRemainingChecked()
464 guard hold <= 0 else { throw Failure.biometryHold(seconds: hold) }
465 switch store.probe(Account.biometryLost) {
466 case .unknown: throw Failure.storage
467 case .present: throw Failure.biometryRequired
468 case .absent: break
469 }
470 }
471
472 /// A espera que falta. Vencida, o registro sai. Ilegivel vale a espera cheia, e a
473 /// abertura do app o regrava a partir de agora (`rebaseHoldLocked`).
474 private func biometryHoldRemainingChecked() throws -> TimeInterval {
475 switch store.probe(Account.biometryHold) {
476 case .absent: return 0
477 case .unknown: throw Failure.storage
478 case .present:
479 guard let data = try? store.read(Account.biometryHold, context: nil), let record = HoldRecord(data) else {
480 return PINPolicy.biometryLossHold
481 }
482 let remaining = record.remaining()
483 if remaining <= 0 { try? store.delete(Account.biometryHold) }
484 return remaining
485 }
486 }
487
488 /// O Face ID caiu sem o dono pedir: a espera e a marca de perdido sao gravadas
489 /// primeiro, e so entao o slot sai. Se uma delas nao gravar, o slot fica: o rosto
490 /// continua sem abrir, e o que pede PIN e rosto continua travado, em vez de passar com
491 /// o PIN so.
492 private func biometryLost() throws {
493 try startHold()
494 if store.probe(Account.biometryLost) != .present {
495 try? store.add(Data([1]), account: Account.biometryLost, protection: .standard, context: nil)
496 }
497 guard store.probe(Account.biometryLost) == .present else { throw Failure.storage }
498 dropBiometrySlot()
499 }
500
501 private func startHold() throws {
502 let record = HoldRecord.startingNow().encoded
503 if (try? store.update(record, account: Account.biometryHold)) == nil {
504 try? store.add(record, account: Account.biometryHold, protection: .standard, context: nil)
505 }
506 guard store.probe(Account.biometryHold) == .present else { throw Failure.storage }
507 }
508
509 /// Depois de reiniciar, o prazo da espera gravado com o relogio monotonico antigo nao
510 /// vale mais: recomeca cheio a partir de agora, como o das tentativas. Registro
511 /// ilegivel tambem.
512 private func rebaseHoldLocked() {
513 guard store.probe(Account.biometryHold) == .present else { return }
514 let record = (try? store.read(Account.biometryHold, context: nil)).flatMap { HoldRecord($0) }
515 guard record == nil || record?.isFromAnotherBoot == true else { return }
516 try? store.update(HoldRecord.startingNow().encoded, account: Account.biometryHold)
517 }
518
519 /// Quanto falta de espera, para a tela de bloqueio mostrar antes do toque. So le.
520 public func throttleRemaining() -> TimeInterval {
521 (try? readAttempts()?.remaining()) ?? 0
522 }
523
524 public func failureCount() -> UInt32 {
525 (try? readAttempts()?.failures) ?? 0
526 }
527
528 /// Depois de reiniciar, o prazo gravado com o relogio monotonico antigo nao vale
529 /// mais: a espera recomeca cheia a partir de agora. Chamado ao abrir e ao voltar
530 /// para o app, os unicos pontos que gravam fora de uma tentativa.
531 public func rebaseAttempts() {
532 lock.lock(); defer { lock.unlock() }
533 _ = try? rebasedAttemptsLocked()
534 rebaseHoldLocked()
535 rebaseHardenedHoldLocked()
536 sweepHardenedLocked()
537 // O slot do rosto sem a chave do SE (cadastro de rostos trocado): a queda conta a
538 // partir de quando o app a ve, nao de quando alguem resolve usar o Face ID.
539 if securityMode == .normal, store.probe(Account.rootByBiometry) == .present, !wrapper.hasKey(.biometry) { try? biometryLost() }
540 }
541
542 // MARK: Biometria
543
544 /// O PIN foi digitado neste boot? Depois de reiniciar, o Face ID espera o PIN.
545 public var pinEnteredThisBoot: Bool {
546 guard let saved = try? store.read(Account.pinBoot, context: nil) else { return false }
547 return PINPolicy.pinRecordValid([UInt8](saved), boot: PINPolicy.bootSession, uptime: PINPolicy.uptime)
548 }
549
550 private func recordPINThisBoot() {
551 let current = Data(PINPolicy.pinRecord(boot: PINPolicy.bootSession, uptime: PINPolicy.uptime))
552 if (try? store.update(current, account: Account.pinBoot)) == nil {
553 try? store.add(current, account: Account.pinBoot, protection: .standard, context: nil)
554 }
555 }
556
557 /// O slot do rosto existe (ou o chaveiro nao sabe dizer). A chave do SE pode ter
558 /// sumido: e o primeiro uso do Face ID que descobre e comeca a espera. Antes era
559 /// "slot e chave", e com a chave invalidada por um rosto novo o app mostrava o Face ID
560 /// desligado, e religar sobrescrevia o slot do dono sem espera (revisao do A1).
561 public var isBiometryEnabled: Bool { securityMode != .hardened && store.probe(Account.rootByBiometry) != .absent }
562
563 /// Liga o Face ID e confere na hora: o slot novo tem de abrir com o rosto e devolver a
564 /// mesma RK. Exige a RK aberta, ou seja, o PIN digitado agora.
565 ///
566 /// Religar por cima de um slot que existe conta como queda, e religar depois de uma
567 /// queda comeca a espera do A1 a partir de agora: quem cadastrou o proprio rosto no
568 /// iPhone nao ve a frase na hora so por ligar o Face ID com o PIN. O primeiro Face ID
569 /// de um cofre que nunca o teve nao espera: antes dele, o PIN ja abria tudo.
570 public func enableBiometry(rk: SecureBytes, reason: String = "") throws {
571 guard securityMode == .normal else { throw Failure.hardenedModeActive }
572 let slot = store.probe(Account.rootByBiometry)
573 let lost = store.probe(Account.biometryLost)
574 guard slot != .unknown, lost != .unknown else { throw Failure.storage }
575 if slot == .present { try biometryLost() }
576 let restarted = slot == .present || lost == .present
577
578 try wrapper.createKey(.biometry)
579 let blob = try wrapper.wrap(rk, slot: .biometry)
580 do {
581 try store.delete(Account.rootByBiometry)
582 try store.add(blob, account: Account.rootByBiometry, protection: .standard, context: nil)
583 try store.delete(Account.biometryState)
584 if let state = Self.biometryDomainState() {
585 try store.add(state, account: Account.biometryState, protection: .standard, context: nil)
586 }
587 } catch {
588 dropBiometrySlot()
589 throw Failure.storage
590 }
591 // A conferencia: sem ela o dono descobriria que o atalho nao funciona no dia em que
592 // precisasse. Falhou, o slot novo sai, e a marca de perdido (se havia) fica.
593 do {
594 let check = try wrapper.unwrap(blob, slot: .biometry, reason: reason)
595 defer { check.wipe() }
596 guard Hash.constantTimeEqual(check, rk) else { throw Failure.storage }
597 } catch {
598 dropBiometrySlot()
599 throw error is Failure ? error : Failure.cancelled
600 }
601 // Religar depois de uma queda: a espera comeca e a marca sai, nessa ordem e
602 // conferidas. Se uma das duas falhar, o slot novo sai e a marca fica: o dono tenta
603 // de novo, em vez de ficar com o Face ID ligado e o que pede os dois recusado para
604 // sempre (segunda revisao do A1).
605 if restarted {
606 do {
607 try startHold()
608 try? store.delete(Account.biometryLost)
609 guard store.probe(Account.biometryLost) == .absent else { throw Failure.storage }
610 } catch {
611 dropBiometrySlot()
612 throw Failure.storage
613 }
614 }
615 }
616
617 /// Desliga o Face ID pelo dono: pede o PIN e o rosto (A1), porque com ele desligado o
618 /// que pedia os dois passa a pedir so o PIN. Nunca toca o slot do PIN.
619 public func disableBiometry(pin: SecureBytes, reason: String) throws {
620 let rk = try unlockWithPINAndBiometry(pin: pin, reason: reason)
621 rk.wipe()
622 dropBiometrySlot()
623 }
624
625 /// Tira o slot do rosto e a chave dele, sem pedir nada: so por dentro do cofre (queda,
626 /// ligar que nao conferiu, desligar depois do PIN e do rosto).
627 func dropBiometrySlot() {
628 try? store.delete(Account.rootByBiometry)
629 try? store.delete(Account.biometryState)
630 wrapper.deleteKey(.biometry)
631 }
632
633 /// O cadastro de biometria mudou desde que o Face ID foi ligado aqui? Sem estado
634 /// gravado ou sem biometria disponivel agora (bloqueio do sistema), a resposta e
635 /// "nao se sabe", tratada como "nao mudou": nada e apagado por duvida.
636 public var biometryEnrollmentChanged: Bool {
637 guard let saved = try? store.read(Account.biometryState, context: nil), let now = Self.biometryDomainState() else {
638 return false
639 }
640 return saved != now
641 }
642
643 static func biometryDomainState() -> Data? {
644 let context = LAContext()
645 defer { context.invalidate() }
646 var error: NSError?
647 guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { return nil }
648 if #available(iOS 18.0, macOS 15.0, *) {
649 return context.domainState.biometry.stateHash
650 }
651 return context.evaluatedPolicyDomainState
652 }
653
654 /// O Face ID deste iPhone visto pelo app agora: sem rosto cadastrado ou tirado do app
655 /// (`gone`), ou presente, inclusive no bloqueio temporario do sistema e em qualquer
656 /// motivo que nao se sabe: nada e apagado por duvida.
657 public enum BiometryAvailability: Sendable { case available, gone }
658
659 static func systemBiometryAvailability() -> BiometryAvailability {
660 let context = LAContext()
661 defer { context.invalidate() }
662 var error: NSError?
663 guard !context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error), let error,
664 error.domain == LAError.errorDomain else { return .available }
665 switch LAError.Code(rawValue: error.code) {
666 case .biometryNotEnrolled?, .biometryNotAvailable?: return .gone
667 default: return .available
668 }
669 }
670
671 // MARK: Troca de PIN
672
673 /// Troca atomica. O slot novo e gravado com sufixo e conferido; o ponto sem volta
674 /// e apagar o slot antigo. Antes dele, qualquer falha desfaz a troca e o PIN antigo
675 /// continua o unico. Depois dele, a troca esta feita: o slot pendente abre com o
676 /// PIN novo, e se a promocao a principal falhar aqui, o proximo desbloqueio a
677 /// conclui. Em nenhum instante o dono fica sem um slot que abre.
678 public func changePIN(rk: SecureBytes, newPIN: SecureBytes) throws {
679 lock.lock(); defer { lock.unlock() }
680 try replacePINLocked(rk: rk, newPIN: newPIN)
681 dropLegacyKeyIfDone()
682 }
683
684 private func replacePINLocked(rk: SecureBytes, newPIN: SecureBytes) throws {
685 guard Self.isWellFormed(newPIN) else { throw Failure.malformedPIN }
686 let pendingAccount = Account.rootByPIN + Account.pendingSuffix
687 // Uma troca interrompida se resolve no proximo desbloqueio, nao aqui.
688 guard store.probe(Account.rootByPIN) == .present else { throw Failure.storage }
689 switch store.probe(pendingAccount) {
690 case .unknown: throw Failure.storage
691 case .present: deleteSlot(suffix: Account.pendingSuffix)
692 case .absent: break
693 }
694
695 do {
696 try writePINSlot(rk: rk, pin: newPIN, kdf: pinParameters, suffix: Account.pendingSuffix)
697 try confirm(pin: newPIN, suffix: Account.pendingSuffix, matches: rk)
698 try store.delete(Account.rootByPIN)
699 } catch {
700 deleteSlot(suffix: Account.pendingSuffix)
701 throw error is Failure ? error : Failure.storage
702 }
703 // Ponto sem volta passado: o PIN antigo nao abre mais nada.
704 try? store.delete(Account.pinKDF)
705 try? store.delete(Account.pinLength)
706 try? promotePendingLocked(rk: rk, pin: newPIN)
707 }
708
709 /// Regrava o slot principal com o PIN que abriu o pendente, confere, e so entao
710 /// apaga o pendente.
711 private func promotePendingLocked(rk: SecureBytes, pin: SecureBytes) throws {
712 if store.probe(Account.rootByPIN) == .present {
713 try store.delete(Account.rootByPIN)
714 }
715 try writePINSlot(rk: rk, pin: pin, kdf: pinParameters, suffix: "")
716 try confirm(pin: pin, suffix: "", matches: rk)
717 deleteSlot(suffix: Account.pendingSuffix)
718 }
719
720 private func deleteSlot(suffix: String) {
721 try? store.delete(Account.rootByPIN + suffix)
722 try? store.delete(Account.pinKDF + suffix)
723 try? store.delete(Account.pinLength + suffix)
724 // A chave do SE do slot pendente vai junto; a do principal, nunca aqui: so a
725 // gravacao de um principal novo a substitui.
726 if !suffix.isEmpty { wrapper.deleteKey(.pinPending) }
727 }
728
729 private func confirm(pin: SecureBytes, suffix: String, matches rk: SecureBytes) throws {
730 let check = try readPINSlot(pin: pin, suffix: suffix)
731 defer { check.wipe() }
732 guard Hash.constantTimeEqual(check, rk) else { throw Failure.storage }
733 }
734
735 // MARK: Apagar
736
737 /// Quantos PINs errados apagam tudo; nil e nunca. O registro antigo (um byte 1)
738 /// valia 10.
739 public var wipeThreshold: UInt32? {
740 guard let data = try? store.read(Account.wipeAfterErrors, context: nil), data.count == 1 else { return nil }
741 let value = UInt32(data[data.startIndex])
742 if value == 1 { return 10 }
743 return PINPolicy.wipeOptions.contains(value) ? value : nil
744 }
745
746 public var wipeAfterErrorsEnabled: Bool { wipeThreshold != nil }
747
748 /// Liga com um dos limites de `PINPolicy.wipeOptions`, ou desliga com nil.
749 public func setWipeAfterErrors(_ threshold: UInt32?) throws {
750 if let threshold, !PINPolicy.wipeOptions.contains(threshold) { throw Failure.storage }
751 do {
752 try store.delete(Account.wipeAfterErrors)
753 if let threshold {
754 try store.add(Data([UInt8(threshold)]), account: Account.wipeAfterErrors, protection: .standard, context: nil)
755 }
756 } catch {
757 throw Failure.storage
758 }
759 }
760
761 /// Destruicao criptografica: as chaves do SE e os slots da RK primeiro (em
762 /// milissegundos todo embrulho vira ruido), depois o resto.
763 public func wipeAll() {
764 lock.lock(); defer { lock.unlock() }
765 wipeAllLocked()
766 }
767
768 private func wipeAllLocked() {
769 WrapSlot.allCases.forEach(wrapper.deleteKey)
770 try? store.delete(Account.rootHardened)
771 try? store.delete(Account.rootByPIN)
772 try? store.delete(Account.rootByPIN + Account.pendingSuffix)
773 try? store.delete(Account.rootByBiometry)
774 try? store.deleteAll()
775 }
776
777 // MARK: Interno
778
779 private func readAttempts() throws -> AttemptRecord? {
780 guard let data = try store.read(Account.attempts, context: nil) else { return nil }
781 guard let record = AttemptRecord(data) else { throw Failure.storage }
782 return record
783 }
784
785 /// O registro atual, com o prazo regravado se ele veio de outro boot. So chamado
786 /// de caminhos que ja gravam. Registro ausente com slot presente e recriado: quem
787 /// consegue apagar item do chaveiro deste app ja executa codigo aqui dentro, e o
788 /// contador nunca foi barreira contra isso.
789 private func rebasedAttemptsLocked() throws -> AttemptRecord {
790 let current: AttemptRecord?
791 do { current = try readAttempts() } catch { throw Failure.storage }
792 guard var record = current else {
793 do {
794 try store.add(AttemptRecord.zero.encoded, account: Account.attempts, protection: .standard, context: nil)
795 } catch {
796 throw Failure.storage
797 }
798 return .zero
799 }
800 if record.isFromAnotherBoot {
801 record = AttemptRecord.after(failures: record.failures)
802 do { try store.update(record.encoded, account: Account.attempts) } catch { throw Failure.storage }
803 }
804 return record
805 }
806
807 /// Grava um slot do PIN. Pre-condicao: o slot nao existe. O registro de custo e
808 /// gravado antes e desfeito se o slot falhar, para nunca sobrar um slot sem custo.
809 func writePINSlot(rk: SecureBytes, pin: SecureBytes, kdf: KDFParameters, suffix: String) throws {
810 guard store.probe(Account.rootByPIN + suffix) == .absent else { throw Failure.storage }
811 let saltBytes = try SecureBytes.random(count: VaultFormat.saltLength)
812 let salt = saltBytes.withUnsafeBytes { Array($0) }
813 let ap = try KeyDerivation.deriveMasterKey(password: pin, salt: salt, parameters: kdf)
814 defer { ap.wipe() }
815
816 let inner = try Self.sealInner(rk, ap: ap, salt: salt)
817 defer { inner.wipe() }
818 // Formato 2: a chave do SE deste slot so desembrulha com a senha derivada do PIN.
819 let slot = Self.wrapSlot(suffix)
820 let password = Self.devicePassword(ap: ap, salt: salt)
821 defer { password.wipe() }
822 try password.withUnsafeData { try wrapper.createKey(slot, password: $0) }
823 let blob: Data
824 do {
825 blob = try password.withUnsafeData { try wrapper.wrap(inner, slot: slot, password: $0) }
826 } catch {
827 wrapper.deleteKey(slot)
828 throw error
829 }
830
831 var kdfRecord = salt
832 kdfRecord += kdf.memoryKiB.bigEndianByteArray + kdf.passes.bigEndianByteArray + [kdf.lanes]
833 kdfRecord.append(SlotFormat.version)
834 try? store.delete(Account.pinKDF + suffix)
835 try? store.delete(Account.pinLength + suffix)
836 do {
837 // O tamanho vai antes do slot e sai junto se ele falhar: nunca existe um slot
838 // de 4 digitos que a tela trate como de 6.
839 try store.add(Data([UInt8(pin.count)]), account: Account.pinLength + suffix, protection: .standard, context: nil)
840 try store.add(Data(kdfRecord), account: Account.pinKDF + suffix, protection: .standard, context: nil)
841 let context = try store.applicationPasswordContext(ap)
842 try store.add(blob, account: Account.rootByPIN + suffix, protection: .applicationPassword, context: context)
843 } catch {
844 try? store.delete(Account.pinKDF + suffix)
845 try? store.delete(Account.pinLength + suffix)
846 wrapper.deleteKey(slot)
847 throw error
848 }
849 }
850
851 /// So para os testes da migracao: grava um slot no formato 1 (chave do aparelho sem
852 /// senha), como as versoes anteriores gravavam.
853 func writeLegacyPINSlot(rk: SecureBytes, pin: SecureBytes, kdf: KDFParameters) throws {
854 if !wrapper.hasKey(.device) { try wrapper.createKey(.device) }
855 let saltBytes = try SecureBytes.random(count: VaultFormat.saltLength)
856 let salt = saltBytes.withUnsafeBytes { Array($0) }
857 let ap = try KeyDerivation.deriveMasterKey(password: pin, salt: salt, parameters: kdf)
858 defer { ap.wipe() }
859 let inner = try Self.sealInner(rk, ap: ap, salt: salt)
860 defer { inner.wipe() }
861 let blob = try wrapper.wrap(inner, slot: .device)
862 var kdfRecord = salt
863 kdfRecord += kdf.memoryKiB.bigEndianByteArray + kdf.passes.bigEndianByteArray + [kdf.lanes]
864 try store.add(Data(kdfRecord), account: Account.pinKDF, protection: .standard, context: nil)
865 try store.add(blob, account: Account.rootByPIN, protection: .applicationPassword, context: store.applicationPasswordContext(ap))
866 if store.probe(Account.attempts) == .absent {
867 try store.add(AttemptRecord.zero.encoded, account: Account.attempts, protection: .standard, context: nil)
868 }
869 }
870
871 private func readPINSlot(pin: SecureBytes, suffix: String) throws -> SecureBytes {
872 guard let record = try store.read(Account.pinKDF + suffix, context: nil),
873 record.count == SlotFormat.legacyLength || (record.count == SlotFormat.length && record.last == SlotFormat.version)
874 else {
875 throw Failure.storage
876 }
877 let version = record.count == SlotFormat.length ? 2 : 1
878 let bytes = [UInt8](record)
879 let salt = Array(bytes[0..<16])
880 let memory = bytes[16..<20].reduce(UInt32(0)) { $0 << 8 | UInt32($1) }
881 let passes = bytes[20..<24].reduce(UInt32(0)) { $0 << 8 | UInt32($1) }
882 let ap = try KeyDerivation.deriveMasterKey(
883 password: pin, salt: salt,
884 parameters: KDFParameters(memoryKiB: memory, passes: passes, lanes: bytes[24])
885 )
886 defer { ap.wipe() }
887
888 let blob: Data?
889 do {
890 blob = try store.read(Account.rootByPIN + suffix, context: store.applicationPasswordContext(ap))
891 } catch StoreError.authenticationFailed {
892 throw Failure.wrongPIN(remainingBeforeWipe: nil)
893 }
894 guard let blob else { throw Failure.notSetUp }
895 let inner: SecureBytes
896 if version == 2 {
897 let password = Self.devicePassword(ap: ap, salt: salt)
898 defer { password.wipe() }
899 do {
900 inner = try password.withUnsafeData { try wrapper.unwrap(blob, slot: Self.wrapSlot(suffix), password: $0) }
901 } catch EnclaveError.wrongPassword {
902 throw Failure.wrongPIN(remainingBeforeWipe: nil)
903 }
904 } else {
905 inner = try wrapper.unwrap(blob, slot: .device, reason: nil)
906 }
907 defer { inner.wipe() }
908 return try Self.openInner(inner, ap: ap, salt: salt)
909 }
910
911 static func innerKey(ap: SecureBytes, salt: [UInt8]) -> SymmetricKey {
912 let material = ap.withUnsafeBytes { SymmetricKey(data: $0) }
913 return HKDF<SHA256>.deriveKey(inputKeyMaterial: material, salt: salt, info: Inner.info, outputByteCount: 32)
914 }
915
916 static func sealInner(_ rk: SecureBytes, ap: SecureBytes, salt: [UInt8]) throws -> SecureBytes {
917 let key = innerKey(ap: ap, salt: salt)
918 let box = try rk.withUnsafeData { try ChaChaPoly.seal($0, using: key, authenticating: Inner.info) }
919 let out = SecureBytes(capacity: Inner.length)
920 out.append(Inner.version)
921 box.combined.withUnsafeBytes { out.append(contentsOf: $0.bindMemory(to: UInt8.self)) }
922 return out
923 }
924
925 /// Falha de autenticacao na camada interna e PIN errado: o chaveiro deixou passar
926 /// (senha de aplicativo so como regra de acesso), mas a chave do PIN nao confere.
927 static func openInner(_ inner: SecureBytes, ap: SecureBytes, salt: [UInt8]) throws -> SecureBytes {
928 guard inner.count == Inner.length else { throw Failure.storage }
929 let key = innerKey(ap: ap, salt: salt)
930 let parsed: (version: UInt8, box: ChaChaPoly.SealedBox)? = inner.withUnsafeBytes { raw in
931 guard let box = try? ChaChaPoly.SealedBox(combined: Data(raw[1...])) else { return nil }
932 return (raw[0], box)
933 }
934 guard let parsed, parsed.version == Inner.version else { throw Failure.storage }
935 var plain: Data
936 do {
937 plain = try ChaChaPoly.open(parsed.box, using: key, authenticating: Inner.info)
938 } catch {
939 throw Failure.wrongPIN(remainingBeforeWipe: nil)
940 }
941 let rk = SecureBytes(capacity: 32)
942 plain.withUnsafeMutableBytes { raw in
943 rk.append(contentsOf: raw.bindMemory(to: UInt8.self).withMemoryRebound(to: UInt8.self) { UnsafeBufferPointer($0) })
944 if let base = raw.baseAddress { memset_s(base, raw.count, 0, raw.count) }
945 }
946 return rk
947 }
948
949 /// A forma do PIN (4 ou 6 digitos), sem deixar copia deles para tras.
950 public static func isWellFormed(_ pin: SecureBytes) -> Bool {
951 var digits = pin.withUnsafeBytes { raw in raw.map { $0 &- 0x30 } }
952 defer { digits.withUnsafeMutableBytes { if let base = $0.baseAddress { memset_s(base, $0.count, 0, $0.count) } } }
953 return PINPolicy.isWellFormed(digits)
954 }
955
956 /// AP = Argon2id(PIN, 256 MiB, p=2, t ate cerca de 0,8 s). E o custo que o
957 /// atacante com execucao de codigo no aparelho paga a cada chute: sem ele, 10^6
958 /// PINs sairiam em horas.
959 public static func calibratedPINParameters() -> KDFParameters {
960 let memory: UInt32 = 256 * 1024
961 let lanes: UInt8 = 2
962 for passes: UInt32 in [4, 3, 2] {
963 let estimate = KDFCalibration.estimatedSeconds(for: KDFParameters(memoryKiB: memory, passes: passes, lanes: lanes))
964 if estimate <= 0.8 { return KDFParameters(memoryKiB: memory, passes: passes, lanes: lanes) }
965 }
966 return KDFParameters(memoryKiB: memory, passes: 2, lanes: lanes)
967 }
968}
969
970// MARK: - Modo reforcado
971
972/// Cabecalho do rk.and, em claro e autenticado como AAD da camada interna (docs/seguranca.md
973/// §2.6).
974///
975/// Formato 4: versao (1) | digitos (1) | chave do PIN (1: 0 = a, 1 = b) | camadas (1) | sal
976/// (16) | memoria (4) | passadas (4) | faixas (1) = 29 bytes. As camadas dizem a politica: 1 so
977/// a `kand.bio` (`.lock`), 2 so a `kand.qualquer` (`.continue`), 3 as duas (`.wait`).
978///
979/// Formato 3 (28 bytes, sem o byte das camadas): o de antes das politicas, so a `kand.bio`.
980/// Lido como `.lock`, com o AAD dele, e nunca mais gravado.
981struct HardenedHeader: Equatable {
982 static let version: UInt8 = 4
983 static let length = 29
984 static let aadInfo = Array("escalibur-wallet/v4/rk.and".utf8)
985 static let legacyVersion: UInt8 = 3
986 static let legacyLength = 28
987 static let legacyAADInfo = Array("escalibur-wallet/v3/rk.and".utf8)
988 static let passwordInfo = Data("escalibur-wallet/v3/kand.senha".utf8)
989 static let innerInfo = Data("escalibur-wallet/v3/rk.and.interna".utf8)
990
991 var version: UInt8
992 var pinLength: UInt8
993 var pinKey: WrapSlot
994 var faceChange: RootKeyVault.HardenedFaceChange
995 var salt: [UInt8]
996 var kdf: KDFParameters
997
998 var byteCount: Int { version == Self.legacyVersion ? Self.legacyLength : Self.length }
999
1000 var encoded: [UInt8] {
1001 let key: UInt8 = pinKey == .hardenedPINA ? 0 : 1
1002 let cost = kdf.memoryKiB.bigEndianByteArray + kdf.passes.bigEndianByteArray + [kdf.lanes]
1003 if version == Self.legacyVersion { return [version, pinLength, key] + salt + cost }
1004 return [version, pinLength, key, faceChange.layerCode] + salt + cost
1005 }
1006
1007 var aad: Data { Data((version == Self.legacyVersion ? Self.legacyAADInfo : Self.aadInfo) + encoded) }
1008
1009 /// Um cabecalho novo: sempre no formato 4.
1010 init(pinLength: UInt8, pinKey: WrapSlot, faceChange: RootKeyVault.HardenedFaceChange, salt: [UInt8], kdf: KDFParameters) {
1011 self.version = Self.version
1012 self.pinLength = pinLength
1013 self.pinKey = pinKey
1014 self.faceChange = faceChange
1015 self.salt = salt
1016 self.kdf = kdf
1017 }
1018
1019 /// Le o formato 4 ou o 3. Camadas fora de {1, 2, 3}: nil.
1020 init?(_ bytes: [UInt8]) {
1021 guard let first = bytes.first, first == Self.version || first == Self.legacyVersion else { return nil }
1022 let legacy = first == Self.legacyVersion
1023 guard bytes.count >= (legacy ? Self.legacyLength : Self.length),
1024 PINPolicy.lengths.contains(Int(bytes[1])), bytes[2] <= 1 else { return nil }
1025 let policy: RootKeyVault.HardenedFaceChange
1026 if legacy {
1027 policy = .lock
1028 } else {
1029 guard let parsed = RootKeyVault.HardenedFaceChange(layerCode: bytes[3]) else { return nil }
1030 policy = parsed
1031 }
1032 let start = legacy ? 3 : 4
1033 version = first
1034 pinLength = bytes[1]
1035 pinKey = bytes[2] == 0 ? .hardenedPINA : .hardenedPINB
1036 faceChange = policy
1037 salt = Array(bytes[start..<start + 16])
1038 let memory = bytes[start + 16..<start + 20].reduce(UInt32(0)) { $0 << 8 | UInt32($1) }
1039 let passes = bytes[start + 20..<start + 24].reduce(UInt32(0)) { $0 << 8 | UInt32($1) }
1040 kdf = KDFParameters(memoryKiB: memory, passes: passes, lanes: bytes[start + 24])
1041 }
1042}
1043
1044/// O rk.and inteiro: o cabecalho e, para cada camada de fora da politica, o mesmo miolo
1045/// (`ECIES_{kand.pin}(0x01 ‖ ChaChaPoly(K_int, RK))`) embrulhado pela chave dela.
1046///
1047/// Formato 4: cada camada presente, na ordem `kand.bio`, `kand.qualquer`, como tamanho (2,
1048/// big-endian) ‖ ECIES. Camadas que nao batem com o corpo (sobra, falta, tamanho zero): o
1049/// registro nao e este. Formato 3: so o ECIES da `kand.bio`, sem tamanho.
1050struct HardenedRecord: Equatable {
1051 var header: HardenedHeader
1052 var layers: [WrapSlot: Data]
1053
1054 var encoded: Data {
1055 var out = Data(header.encoded)
1056 if header.version == HardenedHeader.legacyVersion {
1057 out.append(layers[.hardenedBiometry] ?? Data())
1058 return out
1059 }
1060 for slot in header.faceChange.layers {
1061 let blob = layers[slot] ?? Data()
1062 out.append(contentsOf: UInt16(clamping: blob.count).bigEndianByteArray)
1063 out.append(blob)
1064 }
1065 return out
1066 }
1067
1068 static func parse(_ data: Data) throws -> HardenedRecord {
1069 let bytes = [UInt8](data)
1070 guard let header = HardenedHeader(bytes) else { throw RootKeyVault.Failure.storage }
1071 var rest = bytes[header.byteCount...]
1072 if header.version == HardenedHeader.legacyVersion {
1073 guard !rest.isEmpty else { throw RootKeyVault.Failure.storage }
1074 return HardenedRecord(header: header, layers: [.hardenedBiometry: Data(rest)])
1075 }
1076 var layers: [WrapSlot: Data] = [:]
1077 for slot in header.faceChange.layers {
1078 guard rest.count >= 2 else { throw RootKeyVault.Failure.storage }
1079 let size = Int(rest[rest.startIndex]) << 8 | Int(rest[rest.startIndex + 1])
1080 rest = rest.dropFirst(2)
1081 guard size > 0, rest.count >= size else { throw RootKeyVault.Failure.storage }
1082 layers[slot] = Data(rest.prefix(size))
1083 rest = rest.dropFirst(size)
1084 }
1085 guard rest.isEmpty else { throw RootKeyVault.Failure.storage }
1086 return HardenedRecord(header: header, layers: layers)
1087 }
1088}
1089
1090/// A espera do `.wait` (`and.espera`): o prazo (`HoldRecord`, 25 bytes) e o hash do cadastro
1091/// de rostos que a comecou. Vencida, so realinha a vigia para esse mesmo cadastro: um rosto
1092/// cadastrado depois do prazo, sem o app aberto no meio, comeca outra espera em vez de passar
1093/// pela que ja venceu.
1094struct HardenedHold: Equatable {
1095 var hold: HoldRecord
1096 var state: Data
1097
1098 var encoded: Data { hold.encoded + state }
1099 var remaining: TimeInterval { hold.remaining() }
1100
1101 init(hold: HoldRecord, state: Data) {
1102 self.hold = hold
1103 self.state = state
1104 }
1105
1106 init?(_ data: Data) {
1107 guard data.count >= 25, let hold = HoldRecord(Data(data.prefix(25))) else { return nil }
1108 self.hold = hold
1109 self.state = Data(data.dropFirst(25))
1110 }
1111}
1112
1113extension RootKeyVault {
1114 public enum SecurityMode: Sendable, Equatable { case normal, hardened, unknown }
1115
1116 /// O Face ID visto pelo modo reforcado. So `notEnrolled` e terminal, e so na `.lock`; o
1117 /// resto volta.
1118 public enum BiometryStatus: Sendable, Equatable { case ready, lockedOut, notEnrolled, unavailable }
1119
1120 /// O que o modo reforcado faz quando o cadastro de rostos muda, escolhido pelo dono em
1121 /// "Quando o Face ID mudar" (docs/seguranca.md §2.6). Contra quem nao sabe o codigo do
1122 /// iPhone as tres valem igual: cadastrar um rosto pede o codigo.
1123 public enum HardenedFaceChange: Sendable, Equatable, CaseIterable {
1124 /// Continuar normal, o padrao: a camada de fora e so a `kand.qualquer`
1125 /// (`.biometryAny`), e um rosto novo abre tudo com o PIN, na hora. Quem sabe o PIN e
1126 /// o codigo do iPhone cadastra o proprio rosto e leva tudo.
1127 case `continue`
1128 /// Esperar 24 horas: a `kand.qualquer` abre, e a vigia `kand.bio`
1129 /// (`.biometryCurrentSet`), testada na mesma sessao do rosto, diz se o cadastro
1130 /// mudou. Mudou: o app abre e mostra o saldo, e o resto espera 24 horas.
1131 case wait
1132 /// Bloquear as carteiras: so a `kand.bio`, o comportamento de antes das politicas. Um
1133 /// rosto novo fecha o cofre de vez, e so as palavras trazem de volta.
1134 case lock
1135
1136 /// As camadas de fora, na ordem do corpo do rk.and.
1137 var layers: [WrapSlot] {
1138 switch self {
1139 case .continue: return [.hardenedBiometryAny]
1140 case .wait: return [.hardenedBiometry, .hardenedBiometryAny]
1141 case .lock: return [.hardenedBiometry]
1142 }
1143 }
1144
1145 /// A chave que o Face ID da chamada abre.
1146 var faceKey: WrapSlot { self == .lock ? .hardenedBiometry : .hardenedBiometryAny }
1147
1148 var layerCode: UInt8 {
1149 switch self {
1150 case .lock: return 1
1151 case .continue: return 2
1152 case .wait: return 3
1153 }
1154 }
1155
1156 init?(layerCode: UInt8) {
1157 switch layerCode {
1158 case 1: self = .lock
1159 case 2: self = .continue
1160 case 3: self = .wait
1161 default: return nil
1162 }
1163 }
1164 }
1165
1166 /// Para que o cofre reforcado e aberto. Na espera do `.wait`, so abrir o app passa: o
1167 /// saldo aparece, e o resto espera.
1168 public enum HardenedUse: Sendable { case openApp, operation }
1169
1170 /// Todas as chaves do modo reforcado.
1171 static let hardenedKeys: [WrapSlot] = [.hardenedBiometry, .hardenedBiometryAny, .hardenedPINA, .hardenedPINB]
1172
1173 static func systemBiometryStatus() -> BiometryStatus {
1174 let context = LAContext()
1175 defer { context.invalidate() }
1176 var error: NSError?
1177 if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { return .ready }
1178 guard let error, error.domain == LAError.errorDomain else { return .ready }
1179 switch LAError.Code(rawValue: error.code) {
1180 case .biometryLockout?: return .lockedOut
1181 case .biometryNotEnrolled?: return .notEnrolled
1182 case .biometryNotAvailable?: return .unavailable
1183 default: return .ready // o SEP decide; nada e recusado nem apagado por duvida
1184 }
1185 }
1186
1187 /// Reforcado so com o rk.and presente e nenhum slot do PIN: o commit de ligar e
1188 /// apagar o rk.pin. Com os dois, a transicao nao terminou e o modo normal manda.
1189 public var securityMode: SecurityMode {
1190 let probes = [Account.rootHardened, Account.rootByPIN, Account.rootByPIN + Account.pendingSuffix].map(store.probe)
1191 if probes.contains(.unknown) { return .unknown }
1192 return probes[0] == .present && probes[1] == .absent && probes[2] == .absent ? .hardened : .normal
1193 }
1194
1195 /// Sobrou alguma coisa de uma instalacao (slot, chave do SE)? Para a limpeza de
1196 /// reinstalacao, sem o app conhecer nomes de conta.
1197 public var hasResidue: Bool {
1198 let accounts = [Account.rootByPIN, Account.rootByPIN + Account.pendingSuffix, Account.rootByBiometry, Account.rootHardened]
1199 return accounts.map(store.probe).contains { $0 != .absent } || WrapSlot.allCases.contains { wrapper.probeKey($0) != .absent }
1200 }
1201
1202 /// A politica do modo reforcado, do cabecalho do rk.and. Nil fora do reforcado ou com o
1203 /// cabecalho ilegivel. So le.
1204 public var hardenedFaceChange: HardenedFaceChange? {
1205 guard securityMode == .hardened, let record = try? readHardened() else { return nil }
1206 return record.header.faceChange
1207 }
1208
1209 /// Quanto falta da espera do `.wait`, para a tela avisar antes do teclado. Zero sem espera
1210 /// e fora do reforcado; chaveiro mudo ou registro ilegivel, a espera cheia, nunca zero. So
1211 /// le.
1212 public var hardenedHoldRemaining: TimeInterval {
1213 switch securityMode {
1214 case .normal: return 0
1215 case .unknown: return PINPolicy.biometryLossHold
1216 case .hardened: break
1217 }
1218 if let policy = hardenedFaceChange, policy != .wait { return 0 }
1219 switch store.probe(Account.hardenedHold) {
1220 case .absent: return 0
1221 case .unknown: return PINPolicy.biometryLossHold
1222 case .present:
1223 guard let data = try? store.read(Account.hardenedHold, context: nil), let hold = HardenedHold(data) else {
1224 return PINPolicy.biometryLossHold
1225 }
1226 return hold.remaining
1227 }
1228 }
1229
1230 /// O que abre o cofre pode comecar agora? Na espera do `.wait`, recusa antes do teclado.
1231 /// O cofre confere de novo em `unlockHardened`.
1232 public func checkHardenedAllowed() throws {
1233 let hold = hardenedHoldRemaining
1234 guard hold <= 0 else { throw Failure.biometryHold(seconds: hold) }
1235 }
1236
1237 /// A senha de aplicativo da chave do PIN do modo reforcado. Como `devicePassword`, sai
1238 /// em buffer seguro, que quem chama zera.
1239 static func hardenedPassword(ap: SecureBytes, salt: [UInt8]) -> SecureBytes {
1240 let material = ap.withUnsafeBytes { SymmetricKey(data: $0) }
1241 return secure(HKDF<SHA256>.deriveKey(inputKeyMaterial: material, salt: salt, info: HardenedHeader.passwordInfo, outputByteCount: 32))
1242 }
1243
1244 static func sealHardenedInner(_ rk: SecureBytes, ap: SecureBytes, header: HardenedHeader) throws -> SecureBytes {
1245 let material = ap.withUnsafeBytes { SymmetricKey(data: $0) }
1246 let key = HKDF<SHA256>.deriveKey(inputKeyMaterial: material, salt: header.salt, info: HardenedHeader.innerInfo, outputByteCount: 32)
1247 let box = try rk.withUnsafeData { try ChaChaPoly.seal($0, using: key, authenticating: header.aad) }
1248 let out = SecureBytes(capacity: Inner.length)
1249 out.append(Inner.version)
1250 box.combined.withUnsafeBytes { out.append(contentsOf: $0.bindMemory(to: UInt8.self)) }
1251 return out
1252 }
1253
1254 static func openHardenedInner(_ inner: SecureBytes, ap: SecureBytes, header: HardenedHeader) throws -> SecureBytes {
1255 guard inner.count == Inner.length else { throw Failure.storage }
1256 let material = ap.withUnsafeBytes { SymmetricKey(data: $0) }
1257 let key = HKDF<SHA256>.deriveKey(inputKeyMaterial: material, salt: header.salt, info: HardenedHeader.innerInfo, outputByteCount: 32)
1258 let parsed: (UInt8, ChaChaPoly.SealedBox)? = inner.withUnsafeBytes { raw in
1259 guard let box = try? ChaChaPoly.SealedBox(combined: Data(raw[1...])) else { return nil }
1260 return (raw[0], box)
1261 }
1262 guard let parsed, parsed.0 == Inner.version else { throw Failure.storage }
1263 var plain: Data
1264 do { plain = try ChaChaPoly.open(parsed.1, using: key, authenticating: header.aad) } catch { throw Failure.wrongPIN(remainingBeforeWipe: nil) }
1265 let rk = SecureBytes(capacity: 32)
1266 plain.withUnsafeMutableBytes { raw in
1267 rk.append(contentsOf: UnsafeBufferPointer(raw.bindMemory(to: UInt8.self)))
1268 if let base = raw.baseAddress { memset_s(base, raw.count, 0, raw.count) }
1269 }
1270 return rk
1271 }
1272
1273 func readHardened() throws -> HardenedRecord {
1274 let data: Data?
1275 do { data = try store.read(Account.rootHardened, context: nil) } catch { throw Failure.storage }
1276 guard let data else { throw Failure.notHardened }
1277 return try HardenedRecord.parse(data)
1278 }
1279
1280 // MARK: Abrir
1281
1282 /// PIN e rosto, os dois pela criptografia. Uma chamada = uma tentativa = um Face ID = um
1283 /// PIN avaliado. O contador sobe antes do rosto e so zera com a RK aberta.
1284 ///
1285 /// `use` diz para que: so `AppSession.unlock` passa `.openApp`, o unico uso que a espera
1286 /// do `.wait` deixa passar. Na espera, o resto recusa antes do rosto e sem contar.
1287 public func unlockHardened(pin: SecureBytes, reason: String, use: HardenedUse = .operation) throws -> SecureBytes {
1288 lock.lock(); defer { lock.unlock() }
1289 return try openHardenedLocked(pin: pin, reason: reason, use: use).rk
1290 }
1291
1292 /// A RK aberta e se o cadastro de rostos e o que o reforcado conhece: na `.lock`, a
1293 /// `kand.bio` abriu; na `.wait`, a vigia disse "o mesmo" (ou a espera foi cumprida e a
1294 /// vigia realinhada); na `.continue`, o hash e o gravado quando a politica passou a valer.
1295 private func openHardenedLocked(pin: SecureBytes, reason: String, use: HardenedUse) throws -> (rk: SecureBytes, sameEnrollment: Bool) {
1296 switch securityMode {
1297 case .unknown: throw Failure.storage
1298 case .normal: throw Failure.notHardened
1299 case .hardened: break
1300 }
1301 sweepHardenedLocked()
1302 let record = try readHardened()
1303 let policy = record.header.faceChange
1304 // Tamanho nao e segredo; PIN fora de forma nunca gasta tentativa.
1305 guard Self.isWellFormed(pin), pin.count == Int(record.header.pinLength) else { throw Failure.malformedPIN }
1306 var hold: HardenedHold?
1307 if policy == .wait { hold = try hardenedHoldLocked() }
1308 if let hold, hold.remaining > 0, use == .operation { throw Failure.biometryHold(seconds: hold.remaining) }
1309 try hardenedPreflight(record)
1310
1311 let attempts = try rebasedAttemptsLocked()
1312 let waiting = attempts.remaining()
1313 guard waiting <= 0 else { throw Failure.throttled(seconds: waiting) }
1314 let next = AttemptRecord.after(failures: attempts.failures + 1)
1315 do { try store.update(next.encoded, account: Account.attempts) } catch { throw Failure.storage }
1316
1317 let opened: HardenedOpening
1318 do {
1319 opened = try openHardenedRecord(record, pin: pin, reason: reason)
1320 } catch let failure as Failure {
1321 switch failure {
1322 case .wrongPIN, .biometryNotConfirmed:
1323 if let threshold = wipeThreshold, next.failures >= threshold {
1324 wipeAllLocked()
1325 throw Failure.wiped
1326 }
1327 let left = wipeThreshold.map { $0 - next.failures }
1328 if case .wrongPIN = failure { throw Failure.wrongPIN(remainingBeforeWipe: left) }
1329 throw Failure.biometryNotConfirmed(remainingBeforeWipe: left)
1330 case .hardenedFaceKeyFailed:
1331 // Nenhum rosto foi recusado e o PIN nem foi avaliado: o contador volta ao de
1332 // antes. Se nao gravar, a tentativa fica contada (falha para o lado seguro).
1333 try? store.update(attempts.encoded, account: Account.attempts)
1334 throw failure
1335 default:
1336 throw failure
1337 }
1338 }
1339 defer { opened.middle.wipe() }
1340 try? store.update(AttemptRecord.zero.encoded, account: Account.attempts)
1341 recordPINThisBoot()
1342 // O AAD provou o cabecalho: so agora a varredura pode apagar o que ele diz que sobra.
1343 sweepHardenedKeysLocked(record.header)
1344 switch policy {
1345 case .lock:
1346 refreshHardenedState()
1347 return (opened.rk, true)
1348 case .continue:
1349 return (opened.rk, hardenedStateMatches)
1350 case .wait:
1351 do {
1352 return (opened.rk, try settleWatchLocked(record, opened: opened, hold: hold, use: use))
1353 } catch {
1354 opened.rk.wipe()
1355 throw error
1356 }
1357 }
1358 }
1359
1360 /// O que nao depende do PIN nem gasta tentativa: o Face ID utilizavel agora e as chaves
1361 /// do SE que a chamada usa presentes. A vigia do `.wait` nao entra: ausente, e o que ela
1362 /// tem a dizer.
1363 private func hardenedPreflight(_ record: HardenedRecord) throws {
1364 let policy = record.header.faceChange
1365 switch hardenedBiometryStatus() {
1366 case .lockedOut: throw Failure.biometryLockedOut
1367 case .unavailable: throw Failure.biometryUnavailable
1368 // Sem rosto cadastrado: com a trava, a kand.bio ja morreu; nas outras, a
1369 // kand.qualquer volta a abrir com um rosto cadastrado de novo.
1370 case .notEnrolled: throw policy == .lock ? Failure.hardenedSlotLost : Failure.biometryNotEnrolled
1371 case .ready: break
1372 }
1373 for slot in [policy.faceKey, record.header.pinKey] {
1374 switch wrapper.probeKey(slot) {
1375 case .present: continue
1376 case .absent: throw Failure.hardenedSlotLost
1377 case .unknown: throw Failure.storage
1378 }
1379 }
1380 }
1381
1382 /// O que sai de uma abertura: a RK e o miolo, os dois zerados por quem chama, e o que a
1383 /// vigia do `.wait` viu.
1384 struct HardenedOpening {
1385 let rk: SecureBytes
1386 let middle: SecureBytes
1387 let watch: WatchVerdict?
1388 }
1389
1390 /// A vigia, depois de conferida contra o miolo: abriu o mesmo, a chave nao existe mais, ou
1391 /// nao se sabe.
1392 enum WatchVerdict: Equatable { case same, gone, unknown }
1393
1394 /// rosto (SEP) -> Argon2id -> SE com a senha do PIN -> ChaChaPoly com AAD. Na `.wait`, a
1395 /// vigia na mesma sessao do rosto, sem segundo Face ID; um miolo diferente nela e um
1396 /// registro que nao e o gravado. Nao conta nem grava: quem chama decide. Privado: nada
1397 /// fora do cofre abre "o rosto ja passou".
1398 private func openHardenedRecord(_ record: HardenedRecord, pin: SecureBytes, reason: String) throws -> HardenedOpening {
1399 let header = record.header
1400 let face = header.faceChange.faceKey
1401 guard let outer = record.layers[face] else { throw Failure.storage }
1402 let middle: SecureBytes
1403 var seen: WatchResult?
1404 do {
1405 if header.faceChange == .wait, let watch = record.layers[.hardenedBiometry] {
1406 let pair = try wrapper.unwrap(outer, slot: face, reason: reason, watching: watch, with: .hardenedBiometry)
1407 middle = pair.0
1408 seen = pair.1
1409 } else {
1410 middle = try wrapper.unwrap(outer, slot: face, reason: reason)
1411 }
1412 } catch EnclaveError.failed {
1413 // O SE recusou por um erro que nao vem do Face ID (nenhum rosto foi recusado): nao
1414 // conta. A chave que nao existe mais e o cofre perdido.
1415 if wrapper.probeKey(face) == .absent { throw Failure.hardenedSlotLost }
1416 throw Failure.hardenedFaceKeyFailed
1417 } catch {
1418 // Perdido so com prova: a chave do rosto que o SE ja nao tem. O hash do cadastro
1419 // diferente com a chave presente pode ser so deriva do sistema e conta como o
1420 // rosto que nao confirmou (revisao do modo reforcado, M6).
1421 if wrapper.probeKey(face) == .absent { throw Failure.hardenedSlotLost }
1422 throw Failure.biometryNotConfirmed(remainingBeforeWipe: nil)
1423 }
1424 let watch: WatchVerdict?
1425 switch seen {
1426 case .opened(let other)?:
1427 let equal = Hash.constantTimeEqual(other, middle)
1428 other.wipe()
1429 guard equal else {
1430 middle.wipe()
1431 throw Failure.storage
1432 }
1433 watch = .same
1434 case .gone?: watch = .gone
1435 case .unknown?: watch = .unknown
1436 case nil: watch = nil
1437 }
1438 do {
1439 return HardenedOpening(rk: try openHardenedMiddle(middle, header: header, pin: pin), middle: middle, watch: watch)
1440 } catch {
1441 middle.wipe()
1442 throw error
1443 }
1444 }
1445
1446 private func openHardenedMiddle(_ middle: SecureBytes, header: HardenedHeader, pin: SecureBytes) throws -> SecureBytes {
1447 let ap: SecureBytes
1448 do { ap = try KeyDerivation.deriveMasterKey(password: pin, salt: header.salt, parameters: header.kdf) } catch { throw Failure.storage }
1449 defer { ap.wipe() }
1450 let password = Self.hardenedPassword(ap: ap, salt: header.salt)
1451 defer { password.wipe() }
1452 let inner: SecureBytes
1453 do {
1454 inner = try middle.withUnsafeData { body in
1455 try password.withUnsafeData { try wrapper.unwrap(body, slot: header.pinKey, password: $0) }
1456 }
1457 } catch EnclaveError.wrongPassword {
1458 throw Failure.wrongPIN(remainingBeforeWipe: nil)
1459 } catch EnclaveError.keyMissing {
1460 // O wrapper diz "sem chave" para qualquer falha da busca: perdido so se o SE
1461 // confirmar que ela nao existe.
1462 throw wrapper.probeKey(header.pinKey) == .absent ? Failure.hardenedSlotLost : Failure.storage
1463 } catch {
1464 throw Failure.storage
1465 }
1466 defer { inner.wipe() }
1467 return try Self.openHardenedInner(inner, ap: ap, header: header)
1468 }
1469
1470 /// O que a vigia do `.wait` viu, ja com a RK aberta (PIN e rosto certos).
1471 ///
1472 /// - Mesmo cadastro: a vigia abriu o mesmo miolo, ou nao respondeu e o hash e o gravado.
1473 /// O hash e regravado, e uma espera vencida termina (um realinhamento que caiu antes de
1474 /// apagar a espera).
1475 /// - Troca: a chave da vigia nao existe mais, ou nao respondeu e o hash e outro (ou falta):
1476 /// sem espera, ela comeca, conferida; com a espera correndo, nada muda (so abrir o app
1477 /// chega aqui); vencida para o cadastro que a comecou, a vigia realinha; vencida para
1478 /// outro, outra espera comeca. Fora de abrir o app, a espera comecada agora recusa.
1479 ///
1480 /// Devolve se o cadastro e o conhecido.
1481 private func settleWatchLocked(_ record: HardenedRecord, opened: HardenedOpening, hold: HardenedHold?, use: HardenedUse) throws -> Bool {
1482 let now = hardenedDomainState()
1483 let changed: Bool
1484 switch opened.watch {
1485 case .same?:
1486 changed = false
1487 case .unknown?:
1488 let saved = try? store.read(Account.hardenedState, context: nil)
1489 changed = saved == nil || now == nil || saved != now
1490 case .gone?, nil:
1491 changed = true
1492 }
1493 if !changed {
1494 // So a prova do SEP (a vigia abriu o mesmo miolo) regrava o hash e termina uma
1495 // espera vencida. O hash sozinho nunca encerra espera.
1496 if opened.watch == .same {
1497 refreshHardenedState()
1498 if let hold, hold.remaining <= 0 { try? store.delete(Account.hardenedHold) }
1499 }
1500 return true
1501 }
1502 if let hold {
1503 // Sem o hash de agora, nao se sabe para que cadastro a espera vale: correndo, ela
1504 // fica como esta; vencida, nada abre ate o hash voltar (nem recomeca uma espera de
1505 // cadastro nenhum).
1506 guard let now else {
1507 if hold.remaining > 0 { return false }
1508 throw Failure.biometryUnavailable
1509 }
1510 if hold.remaining > 0 {
1511 // Outro cadastro no meio da espera (o dono redefiniu o Face ID, ou alguem
1512 // cadastrou mais um rosto): ela recomeca cheia, para o cadastro de agora, e a
1513 // faixa diz o prazo certo. So abrir o app chega aqui.
1514 if hold.state != now { try startHardenedHold(state: now) }
1515 return false
1516 }
1517 if !hold.state.isEmpty, hold.state == now {
1518 // Uma queda em qualquer passo termina na proxima abertura: a espera vencida
1519 // continua la, e a vigia continua vendo a troca.
1520 try? realignWatchLocked(record, middle: opened.middle, state: now)
1521 return true
1522 }
1523 }
1524 try startHardenedHold(state: now)
1525 if use == .operation { throw Failure.biometryHold(seconds: PINPolicy.biometryLossHold) }
1526 return false
1527 }
1528
1529 /// A espera venceu para o cadastro que a comecou: a vigia passa a acompanhar o de agora.
1530 /// Uma `kand.bio` nova, o mesmo miolo embrulhado por ela, o `update` do rk.and conferido
1531 /// lendo de volta, o hash regravado e a espera apagada, nessa ordem. A `kand.qualquer` e a
1532 /// camada dela nao sao tocadas: em nenhum passo o registro deixa de abrir.
1533 private func realignWatchLocked(_ record: HardenedRecord, middle: SecureBytes, state: Data) throws {
1534 try wrapper.createKey(.hardenedBiometry)
1535 var updated = record
1536 updated.layers[.hardenedBiometry] = try wrapper.wrap(middle, slot: .hardenedBiometry)
1537 let data = updated.encoded
1538 try? store.update(data, account: Account.rootHardened)
1539 guard (try? store.read(Account.rootHardened, context: nil)) == data else { throw Failure.storage }
1540 writeHardenedState(state)
1541 try? store.delete(Account.hardenedHold)
1542 }
1543
1544 /// A espera do `.wait`, lida depois de rebaseada. Nil sem espera; chaveiro mudo recusa.
1545 private func hardenedHoldLocked() throws -> HardenedHold? {
1546 switch store.probe(Account.hardenedHold) {
1547 case .absent: return nil
1548 case .unknown: throw Failure.storage
1549 case .present: break
1550 }
1551 rebaseHardenedHoldLocked()
1552 guard let data = try? store.read(Account.hardenedHold, context: nil), let hold = HardenedHold(data) else {
1553 return HardenedHold(hold: .startingNow(), state: Data())
1554 }
1555 return hold
1556 }
1557
1558 /// A troca vista agora: a espera comeca cheia, com o hash do cadastro que a comecou, e so
1559 /// vale se ficou gravada como foi escrita.
1560 private func startHardenedHold(state: Data?) throws {
1561 let record = HardenedHold(hold: .startingNow(), state: state ?? Data()).encoded
1562 if (try? store.update(record, account: Account.hardenedHold)) == nil {
1563 try? store.add(record, account: Account.hardenedHold, protection: .standard, context: nil)
1564 }
1565 guard (try? store.read(Account.hardenedHold, context: nil)) == record else { throw Failure.storage }
1566 }
1567
1568 /// Depois de reiniciar, o prazo gravado com o relogio monotonico antigo nao vale mais: a
1569 /// espera do `.wait` recomeca cheia a partir de agora, para o mesmo cadastro. Ilegivel,
1570 /// recomeca de cadastro nenhum (vencida, comeca outra).
1571 func rebaseHardenedHoldLocked() {
1572 guard store.probe(Account.hardenedHold) == .present else { return }
1573 let hold = (try? store.read(Account.hardenedHold, context: nil)).flatMap { HardenedHold($0) }
1574 guard hold == nil || hold?.hold.isFromAnotherBoot == true else { return }
1575 try? store.update(HardenedHold(hold: .startingNow(), state: hold?.state ?? Data()).encoded, account: Account.hardenedHold)
1576 }
1577
1578 /// O hash do cadastro de rostos diferente do gravado. Nunca decide nada sozinho: so a
1579 /// mensagem do rosto que nao confirmou diz que so as palavras trazem de volta (na trava, se
1580 /// o cadastro mudou; nas outras, se o Face ID foi redefinido).
1581 public var hardenedEnrollmentChanged: Bool {
1582 guard let saved = try? store.read(Account.hardenedState, context: nil), let now = hardenedDomainState() else { return false }
1583 return saved != now
1584 }
1585
1586 /// O hash de agora e o gravado quando a politica passou a valer. Sem um dos dois, nao: na
1587 /// duvida, conta como troca.
1588 private var hardenedStateMatches: Bool {
1589 guard let saved = try? store.read(Account.hardenedState, context: nil), let now = hardenedDomainState() else { return false }
1590 return saved == now
1591 }
1592
1593 /// A `kand.bio` abriu: o cadastro e o de quando a chave nasceu. Um hash diferente era so
1594 /// deriva do sistema.
1595 private func refreshHardenedState() {
1596 guard let now = hardenedDomainState() else { return }
1597 writeHardenedState(now)
1598 }
1599
1600 private func writeHardenedState(_ state: Data) {
1601 if (try? store.read(Account.hardenedState, context: nil)) == state { return }
1602 try? store.delete(Account.hardenedState)
1603 try? store.add(state, account: Account.hardenedState, protection: .standard, context: nil)
1604 }
1605
1606 // MARK: Gravar e conferir
1607
1608 /// Monta o registro com o PIN e a geracao `pinKey` da chave do PIN (criada aqui, no lugar
1609 /// da que houver nela), e embrulha o mesmo miolo por cada camada de fora de `faceChange`.
1610 /// As chaves de fora ja existem: quem chama decide quais nascem agora e quais o registro
1611 /// em vigor ja usa (essas nunca sao recriadas). A `kand.qualquer` embrulha o miolo aqui e
1612 /// em nenhum outro lugar, e nunca a RK (verificar.sh).
1613 private func buildHardenedRecord(rk: SecureBytes, pin: SecureBytes, pinKey: WrapSlot, faceChange: HardenedFaceChange) throws -> Data {
1614 let saltBytes = try SecureBytes.random(count: VaultFormat.saltLength)
1615 let salt = saltBytes.withUnsafeBytes { Array($0) }
1616 let kdf = pinParameters
1617 let ap = try KeyDerivation.deriveMasterKey(password: pin, salt: salt, parameters: kdf)
1618 defer { ap.wipe() }
1619 let header = HardenedHeader(pinLength: UInt8(pin.count), pinKey: pinKey, faceChange: faceChange, salt: salt, kdf: kdf)
1620 let password = Self.hardenedPassword(ap: ap, salt: salt)
1621 defer { password.wipe() }
1622 try password.withUnsafeData { try wrapper.createKey(pinKey, password: $0) }
1623 let inner = try Self.sealHardenedInner(rk, ap: ap, header: header)
1624 defer { inner.wipe() }
1625 var middleData = try password.withUnsafeData { try wrapper.wrap(inner, slot: pinKey, password: $0) }
1626 let middle = SecureBytes(capacity: middleData.count)
1627 middleData.withUnsafeMutableBytes { raw in
1628 middle.append(contentsOf: raw.bindMemory(to: UInt8.self).withMemoryRebound(to: UInt8.self) { UnsafeBufferPointer($0) })
1629 // O miolo e o "rosto ja passou": nao fica num Data solto.
1630 if let base = raw.baseAddress { memset_s(base, raw.count, 0, raw.count) }
1631 }
1632 defer { middle.wipe() }
1633 var layers: [WrapSlot: Data] = [:]
1634 if faceChange.layers.contains(.hardenedBiometry) {
1635 layers[.hardenedBiometry] = try wrapper.wrap(middle, slot: .hardenedBiometry)
1636 }
1637 if faceChange.layers.contains(.hardenedBiometryAny) {
1638 layers[.hardenedBiometryAny] = try wrapper.wrap(middle, slot: .hardenedBiometryAny)
1639 }
1640 return HardenedRecord(header: header, layers: layers).encoded
1641 }
1642
1643 /// A prova de que o registro abre com este PIN e este rosto e devolve esta RK; na `.wait`,
1644 /// tambem que a vigia abre na mesma sessao (ou, sem resposta dela, que o cadastro de agora
1645 /// e `state`). Nao conta: so roda com a RK ja aberta.
1646 private func verifyHardened(_ data: Data, pin: SecureBytes, reason: String, matches rk: SecureBytes, state: Data?) throws {
1647 let record = try HardenedRecord.parse(data)
1648 let opened = try openHardenedRecord(record, pin: pin, reason: reason)
1649 defer {
1650 opened.rk.wipe()
1651 opened.middle.wipe()
1652 }
1653 guard Hash.constantTimeEqual(opened.rk, rk) else { throw Failure.storage }
1654 switch opened.watch {
1655 case nil, .same?:
1656 return
1657 case .unknown?:
1658 guard let state, hardenedDomainState() == state else { throw Failure.storage }
1659 case .gone?:
1660 throw Failure.storage
1661 }
1662 }
1663
1664 private func discardHardenedLocked() {
1665 try? store.delete(Account.rootHardened)
1666 try? store.delete(Account.hardenedState)
1667 try? store.delete(Account.hardenedHold)
1668 Self.hardenedKeys.forEach(wrapper.deleteKey)
1669 }
1670
1671 /// Leva o que sobrou de uma transicao para o estado que o modo diz. So com respostas
1672 /// definitivas; com os dois slots, nada (o PIN certo resolve).
1673 ///
1674 /// No reforcado, so as sobras do modo normal: as chaves do reforcado dependem do
1675 /// cabecalho, e o cabecalho em claro so vale depois de autenticado
1676 /// (`sweepHardenedKeysLocked`). Antes, isto lia o cabecalho sem autenticar, e trocar o
1677 /// byte das camadas entre 1 e 2 fazia a volta ao app apagar a chave de fora em vigor
1678 /// (revisao das tres politicas, B3).
1679 func sweepHardenedLocked() {
1680 switch securityMode {
1681 case .hardened:
1682 for account in [Account.rootByBiometry, Account.biometryState, Account.biometryHold, Account.biometryLost, Account.pinKDF, Account.pinLength,
1683 Account.pinKDF + Account.pendingSuffix, Account.pinLength + Account.pendingSuffix] {
1684 try? store.delete(account)
1685 }
1686 [WrapSlot.device, .pin, .pinPending, .biometry].forEach(wrapper.deleteKey)
1687 case .normal:
1688 guard store.probe(Account.rootHardened) == .absent else { return }
1689 Self.hardenedKeys.forEach(wrapper.deleteKey)
1690 try? store.delete(Account.hardenedState)
1691 try? store.delete(Account.hardenedHold)
1692 case .unknown:
1693 return
1694 }
1695 }
1696
1697 /// O que um cabecalho autenticado diz que sobra: a geracao da chave do PIN que ele nao usa,
1698 /// as chaves de fora que a politica dele nao usa (`.lock`: a `kand.qualquer`; `.continue`:
1699 /// a `kand.bio`; `.wait`: nenhuma, nem a `kand.bio` morta, que o realinhamento recria), e a
1700 /// espera fora da `.wait`. So com `header` provado: depois de uma abertura (o AAD da camada
1701 /// interna o autentica) ou de um commit que o cofre acabou de gravar e conferir. Antes do
1702 /// commit de uma troca o cabecalho antigo manda, depois o novo: o registro em vigor nunca
1703 /// aponta para uma chave que isto apaga.
1704 private func sweepHardenedKeysLocked(_ header: HardenedHeader) {
1705 let policy = header.faceChange
1706 wrapper.deleteKey(header.pinKey == .hardenedPINA ? .hardenedPINB : .hardenedPINA)
1707 for slot in [WrapSlot.hardenedBiometry, .hardenedBiometryAny] where !policy.layers.contains(slot) {
1708 wrapper.deleteKey(slot)
1709 }
1710 if policy != .wait { try? store.delete(Account.hardenedHold) }
1711 }
1712
1713 // MARK: Ligar, desligar, trocar o PIN, trocar a politica
1714
1715 /// Liga. Uma chamada, sincrona, fora do MainActor: PIN e rosto pelo caminho de hoje,
1716 /// `check` com a RK (a conferencia das palavras), as chaves de fora de `faceChange`
1717 /// criadas, o rk.and gravado e aberto de novo com PIN e rosto, e so entao o commit
1718 /// (apagar o rk.pin).
1719 ///
1720 /// `check` roda sob a trava do cofre: nunca pode chamar o `RootKeyVault` (a trava nao e
1721 /// recursiva). Abrir carteiras pelo `WalletVault` pode.
1722 public func enableHardened(pin: SecureBytes, reason: String, confirmReason: String, faceChange: HardenedFaceChange = .continue,
1723 check: (SecureBytes) throws -> Void) throws {
1724 lock.lock(); defer { lock.unlock() }
1725 switch securityMode {
1726 case .hardened: throw Failure.hardenedModeActive
1727 case .unknown: throw Failure.storage
1728 case .normal: break
1729 }
1730 guard slotVersion(suffix: "") == 2 else { throw Failure.hardenedBlocked(.legacySlot) }
1731 switch store.probe(Account.rootByPIN + Account.pendingSuffix) {
1732 case .absent: break
1733 case .present: throw Failure.hardenedBlocked(.changePending)
1734 case .unknown: throw Failure.storage
1735 }
1736 guard store.probe(Account.rootByBiometry) == .present, wrapper.probeKey(.biometry) == .present else {
1737 throw Failure.hardenedBlocked(.biometryOff)
1738 }
1739 let hold = try biometryHoldRemainingChecked()
1740 guard hold <= 0 else { throw Failure.biometryHold(seconds: hold) }
1741 guard hardenedBiometryStatus() == .ready else { throw Failure.hardenedBlocked(.biometryUnavailable) }
1742 let stateBefore = hardenedDomainState()
1743
1744 // 1. PIN e o rosto do slot de hoje (A1): prova que o cadastro e o de quando o Face
1745 // ID foi ligado, e conta tentativa como sempre.
1746 let rk = try unlockWithPINAndBiometryLocked(pin: pin, reason: reason)
1747 defer { rk.wipe() }
1748 try check(rk)
1749 // 2. Sobras de uma ativacao interrompida: so agora, com o slot do PIN provado vivo.
1750 discardHardenedLocked()
1751 // 3. Chaves novas e o registro.
1752 let committed: HardenedHeader
1753 do {
1754 for slot in faceChange.layers { try wrapper.createKey(slot) }
1755 let record = try buildHardenedRecord(rk: rk, pin: pin, pinKey: .hardenedPINA, faceChange: faceChange)
1756 try store.add(record, account: Account.rootHardened, protection: .standard, context: nil)
1757 if let state = stateBefore { try store.add(state, account: Account.hardenedState, protection: .standard, context: nil) }
1758 // 4. Conferir lendo do chaveiro e abrindo de verdade: segundo Face ID.
1759 guard let stored = try store.read(Account.rootHardened, context: nil) else { throw Failure.storage }
1760 try verifyHardened(stored, pin: pin, reason: confirmReason, matches: rk, state: stateBefore)
1761 guard hardenedDomainState() == stateBefore else { throw Failure.biometryChanged }
1762 committed = try HardenedRecord.parse(stored).header
1763 } catch {
1764 discardHardenedLocked()
1765 throw error is Failure ? error : Failure.storage
1766 }
1767 // 5. Commit. Daqui em diante nada apaga o rk.and por falha.
1768 try? store.delete(Account.rootByPIN)
1769 guard store.probe(Account.rootByPIN) == .absent else { throw Failure.storage }
1770 // 6. Limpeza; se parar no meio, a varredura e a proxima abertura terminam.
1771 sweepHardenedLocked()
1772 sweepHardenedKeysLocked(committed)
1773 }
1774
1775 /// Desliga: PIN e rosto no rk.and (fora da espera do `.wait`), o slot do PIN gravado e
1776 /// conferido (a partir dai o modo normal manda), o Face ID religado e conferido (segundo
1777 /// rosto; se falhar, fica desligado), e o rk.and sai. Devolve se o Face ID voltou.
1778 ///
1779 /// O Face ID volta sem a espera do A1 so quando o reforcado reconhece o cadastro: a
1780 /// `kand.bio` abriu (`.lock`), a vigia disse "o mesmo" (`.wait`), ou o hash e o gravado
1781 /// (`.continue`). Senao, a marca de perdido e gravada antes do slot do PIN: o
1782 /// `enableBiometry` comeca a espera ao religar, e sem o Face ID religado o que pede PIN e
1783 /// rosto recusa ate o dono religar, como numa queda do A1.
1784 @discardableResult
1785 public func disableHardened(pin: SecureBytes, reason: String, biometryReason: String) throws -> Bool {
1786 lock.lock(); defer { lock.unlock() }
1787 let (rk, same) = try openHardenedLocked(pin: pin, reason: reason, use: .operation)
1788 defer { rk.wipe() }
1789 if !same {
1790 if store.probe(Account.biometryLost) != .present {
1791 try? store.add(Data([1]), account: Account.biometryLost, protection: .standard, context: nil)
1792 }
1793 guard store.probe(Account.biometryLost) == .present else { throw Failure.storage }
1794 }
1795 do {
1796 try writePINSlot(rk: rk, pin: pin, kdf: pinParameters, suffix: "")
1797 try confirm(pin: pin, suffix: "", matches: rk)
1798 } catch {
1799 // O rk.pin sai e e conferido antes da chave dele: um rk.pin sem chave tiraria o
1800 // cofre do reforcado com um slot que nao abre (revisao do modo reforcado).
1801 if !same { try? store.delete(Account.biometryLost) }
1802 try? store.delete(Account.rootByPIN)
1803 guard store.probe(Account.rootByPIN) == .absent else { throw Failure.storage }
1804 deleteSlot(suffix: "")
1805 wrapper.deleteKey(.pin)
1806 throw error is Failure ? error : Failure.storage
1807 }
1808 // A conferencia do rosto e a do proprio `enableBiometry`; falhou, fica desligado.
1809 let restored = (try? enableBiometry(rk: rk, reason: biometryReason)) != nil
1810 discardHardenedLocked()
1811 return restored
1812 }
1813
1814 /// Troca o PIN no modo reforcado, mantendo a politica e as chaves de fora: a chave do PIN
1815 /// alterna (a/b), o registro novo e aberto com PIN novo e rosto antes, e o commit e a
1816 /// troca no lugar do rk.and (atomica). Na espera do `.wait`, recusa.
1817 public func changeHardenedPIN(currentPIN: SecureBytes, newPIN: SecureBytes, reason: String, confirmReason: String) throws {
1818 lock.lock(); defer { lock.unlock() }
1819 guard Self.isWellFormed(newPIN) else { throw Failure.malformedPIN }
1820 let rk = try openHardenedLocked(pin: currentPIN, reason: reason, use: .operation).rk
1821 defer { rk.wipe() }
1822 try replaceHardenedLocked(rk: rk, pin: newPIN, target: nil, confirmReason: confirmReason)
1823 }
1824
1825 /// Muda o que acontece quando o Face ID muda. PIN e rosto (uma tentativa contada; na
1826 /// espera do `.wait`, recusa antes), `check` com a RK so na ida para `.lock` (com a
1827 /// trava, o papel e o unico caminho de volta: as palavras de cada carteira), o registro
1828 /// novo montado com a outra geracao da chave do PIN e aberto com o segundo rosto, e o
1829 /// commit pelo `update` do rk.and. Sair da `.lock` abre pela `kand.bio`: o rosto de agora
1830 /// prova o mesmo cadastro. A mesma politica nao pede nada.
1831 ///
1832 /// `check` roda sob a trava do cofre: nunca pode chamar o `RootKeyVault`.
1833 public func setHardenedFaceChange(_ target: HardenedFaceChange, pin: SecureBytes, reason: String, confirmReason: String,
1834 check: (SecureBytes) throws -> Void) throws {
1835 lock.lock(); defer { lock.unlock() }
1836 switch securityMode {
1837 case .unknown: throw Failure.storage
1838 case .normal: throw Failure.notHardened
1839 case .hardened: break
1840 }
1841 guard try readHardened().header.faceChange != target else { return }
1842 let rk = try openHardenedLocked(pin: pin, reason: reason, use: .operation).rk
1843 defer { rk.wipe() }
1844 if target == .lock { try check(rk) }
1845 try replaceHardenedLocked(rk: rk, pin: pin, target: target, confirmReason: confirmReason)
1846 }
1847
1848 /// Troca o rk.and no lugar, com a outra geracao da chave do PIN: a maquina da troca de PIN
1849 /// (`target` nil, a mesma politica) e da troca de politica.
1850 ///
1851 /// As chaves de fora que o registro em vigor ja usa sao reaproveitadas, nunca recriadas;
1852 /// as que faltam para a politica nova nascem agora. O registro novo e aberto com o PIN e o
1853 /// rosto (o segundo Face ID, que nao conta tentativa: recusado, volta como cancelado) antes
1854 /// do commit, que e o `update` do rk.and, e o que ficou gravado decide, nao o retorno do
1855 /// update. Antes do commit o cabecalho antigo manda, e a varredura so apaga o que nasceu
1856 /// aqui; depois, o novo, e a varredura apaga a geracao antiga e as chaves de fora que a
1857 /// politica nova nao usa.
1858 private func replaceHardenedLocked(rk: SecureBytes, pin: SecureBytes, target: HardenedFaceChange?, confirmReason: String) throws {
1859 guard let old = try? store.read(Account.rootHardened, context: nil) else { throw Failure.storage }
1860 let current = try HardenedRecord.parse(old)
1861 let from = current.header.faceChange
1862 let to = target ?? from
1863 let nextKey: WrapSlot = current.header.pinKey == .hardenedPINA ? .hardenedPINB : .hardenedPINA
1864 let born = to.layers.filter { !from.layers.contains($0) }
1865 let fresh = [nextKey] + born
1866 let stateBefore = hardenedDomainState()
1867 // A vigia que nasce agora acompanha o cadastro de agora; a reaproveitada, o gravado.
1868 let state: Data? = born.contains(.hardenedBiometry) ? stateBefore : (try? store.read(Account.hardenedState, context: nil)) ?? nil
1869 wrapper.deleteKey(nextKey)
1870 let record: Data
1871 do {
1872 for slot in born { try wrapper.createKey(slot) }
1873 record = try buildHardenedRecord(rk: rk, pin: pin, pinKey: nextKey, faceChange: to)
1874 try verifyHardened(record, pin: pin, reason: confirmReason, matches: rk, state: state)
1875 if born.contains(.hardenedBiometry) {
1876 guard hardenedDomainState() == stateBefore else { throw Failure.biometryChanged }
1877 }
1878 } catch Failure.biometryNotConfirmed {
1879 fresh.forEach(wrapper.deleteKey)
1880 throw Failure.cancelled
1881 } catch {
1882 fresh.forEach(wrapper.deleteKey)
1883 throw error is Failure ? error : Failure.storage
1884 }
1885 try? store.update(record, account: Account.rootHardened)
1886 guard let stored = try? store.read(Account.rootHardened, context: nil) else { throw Failure.storage }
1887 if stored == record {
1888 // Uma chave nova sumiu depois do commit: volta o registro antigo, conferido, antes
1889 // de recusar. Sem isso, o rk.and gravado apontaria para uma chave que nao existe e
1890 // a varredura apagaria as antigas (segunda revisao do modo reforcado, N2).
1891 guard fresh.allSatisfy({ wrapper.probeKey($0) == .present }) else {
1892 try? store.update(old, account: Account.rootHardened)
1893 guard (try? store.read(Account.rootHardened, context: nil)) == old else { throw Failure.storage }
1894 fresh.forEach(wrapper.deleteKey)
1895 throw Failure.storage
1896 }
1897 if born.contains(.hardenedBiometry), let stateBefore { writeHardenedState(stateBefore) }
1898 sweepHardenedLocked()
1899 if let header = try? HardenedRecord.parse(record).header { sweepHardenedKeysLocked(header) }
1900 } else if stored == old {
1901 fresh.forEach(wrapper.deleteKey)
1902 throw Failure.storage
1903 } else {
1904 throw Failure.storage
1905 }
1906 }
1907}