Pular para o conteúdo
Escalibur Wallet
Auditoria

As regras do PIN

Kit/Sources/EscaliburKeys/PINPolicy.swift

As regras do PIN: a forma, a escada de atraso e o relógio que sobrevive a reinicio sem trancar o dono por dias.

241 linhasSHA-256 34d542dd3de62753…Abrir no GitHub, no commit 015f5f5
1import Darwin
2import Foundation
3
4/// As regras do PIN: a forma, a escada de atraso e o relogio que sobrevive a
5/// reinicio sem trancar o dono por dias.
6public enum PINPolicy {
7 /// O tamanho padrao, e o de todo PIN gravado antes de existir a escolha.
8 public static let digits = 6
9
10 /// Os tamanhos que o dono pode escolher (pedido do dono, 16/10/2026: 4 tambem). Com 4,
11 /// sao 10^4 combinacoes em vez de 10^6: contra quem tem execucao de codigo no
12 /// aparelho, minutos em vez de horas (docs/seguranca.md §2.8).
13 public static let lengths = [4, 6]
14
15 /// O PIN e do dono: qualquer combinacao de 4 ou 6 digitos vale, inclusive 1111 e
16 /// 123456. Decisao do dono em 2026-09-26, que retirou a lista de PINs faceis. So a
17 /// forma e conferida: 4 ou 6 digitos de 0 a 9. Contra quem tenta pela interface
18 /// ficam a escada de atraso e, se o dono ligar, o apagar apos erros.
19 public static func isWellFormed(_ digits: [UInt8]) -> Bool {
20 lengths.contains(digits.count) && digits.allSatisfy { $0 <= 9 }
21 }
22
23 /// A escada de atraso, em segundos, depois de `failures` erros seguidos.
24 ///
25 /// Do nono erro em diante a espera dobra a cada erro, de 3 horas ate 7 dias (auditoria
26 /// 4, C7-04). Com o teto antigo de 3 horas, quem tem o iPhone tentava cerca de 2.900
27 /// PINs por ano; com o dobro, 18 em 30 dias e 66 em um ano.
28 public static func delay(afterFailures failures: UInt32) -> TimeInterval {
29 switch failures {
30 case 0...2: return 0
31 case 3: return 5
32 case 4: return 15
33 case 5: return 60
34 case 6: return 5 * 60
35 case 7: return 15 * 60
36 case 8: return 60 * 60
37 default:
38 let doubled = 3 * 60 * 60 * pow(2, Double(min(failures - 9, 16)))
39 return min(doubled, maxDelay)
40 }
41 }
42
43 /// O teto da escada: 7 dias por tentativa.
44 public static let maxDelay: TimeInterval = 7 * 24 * 60 * 60
45
46 /// A espera do A1 depois que o Face ID cai sem o dono pedir (o cadastro de rostos mudou,
47 /// o Face ID foi tirado do app ou do iPhone): durante ela, ver as palavras, lacrar o
48 /// envelope e remover carteira ficam travados. Quem viu o codigo do iPhone e o PIN
49 /// cadastra o proprio rosto e derruba o slot; sem a espera, isso trocava "PIN e rosto"
50 /// por "so o PIN" na hora (auditoria 4, A1).
51 public static let biometryLossHold: TimeInterval = 24 * 60 * 60
52
53 /// Quantos erros podem apagar tudo, a escolha do dono (ou nunca).
54 public static let wipeOptions: [UInt32] = [5, 10, 15, 20]
55
56 // MARK: Relogios
57
58 /// Tempo desde o boot, contando o tempo dormindo, e que nao anda para tras quando
59 /// alguem muda o relogio. O relogio de parede nunca entra na conta da espera:
60 /// adiantar nao encurta, atrasar nao tranca o dono por um ano.
61 public static var uptime: TimeInterval {
62 var time = timespec()
63 clock_gettime(CLOCK_MONOTONIC_RAW, &time)
64 return TimeInterval(time.tv_sec) + TimeInterval(time.tv_nsec) / 1e9
65 }
66
67 /// Identidade deste boot, 16 bytes. Se mudou, o relogio monotonico recomecou do zero
68 /// e o prazo gravado com ele deixou de valer.
69 ///
70 /// No Mac e no simulador e o `kern.bootsessionuuid`, comparado byte a byte. No iPhone
71 /// a sandbox recusa essa leitura (EPERM, medido num iPhone 13 Pro Max com iOS 26.3):
72 /// vale o instante do boot (`kern.boottime`), marcado com `boottimeMark` e comparado
73 /// com folga de `boottimeTolerance` em `isSameBoot`, porque ele oscila um pouco com o
74 /// ajuste de hora pela rede.
75 public static var bootSession: [UInt8] {
76 if let uuid = bootSessionUUID { return withUnsafeBytes(of: uuid.uuid) { Array($0) } }
77 var time = timeval()
78 var size = MemoryLayout<timeval>.size
79 guard sysctlbyname("kern.boottime", &time, &size, nil, 0) == 0, time.tv_sec > 0 else { return unknownBoot }
80 let micros = UInt64(time.tv_sec) * 1_000_000 + UInt64(time.tv_usec)
81 return boottimeMark + (0..<8).map { UInt8(truncatingIfNeeded: micros >> (56 - 8 * $0)) }
82 }
83
84 private static var bootSessionUUID: UUID? {
85 var size = 0
86 guard sysctlbyname("kern.bootsessionuuid", nil, &size, nil, 0) == 0, size > 0 else { return nil }
87 var buffer = [CChar](repeating: 0, count: size)
88 guard sysctlbyname("kern.bootsessionuuid", &buffer, &size, nil, 0) == 0 else { return nil }
89 return UUID(uuidString: String(decoding: buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }, as: UTF8.self))
90 }
91
92 /// Os 8 primeiros bytes de uma identidade tirada do `kern.boottime` ("ESCBOOT1").
93 static let boottimeMark: [UInt8] = Array("ESCBOOT1".utf8)
94
95 /// Reiniciar move o instante do boot pelo tempo que o iPhone ficou ligado mais o da
96 /// propria reinicializacao, sempre bem mais que isto. O ajuste de hora pela rede
97 /// move milissegundos. Mudar a hora a mao parece um boot novo: o PIN volta a ser
98 /// pedido e a espera recomeca cheia, nunca o contrario.
99 static let boottimeTolerance: UInt64 = 10_000_000
100
101 /// As duas identidades sao do mesmo boot? Desconhecida nunca e.
102 public static func isSameBoot(_ a: [UInt8], _ b: [UInt8]) -> Bool {
103 guard a.count == 16, b.count == 16, a != unknownBoot, b != unknownBoot else { return false }
104 guard Array(a.prefix(8)) == boottimeMark, Array(b.prefix(8)) == boottimeMark else { return a == b }
105 let x = a.suffix(8).reduce(UInt64(0)) { $0 << 8 | UInt64($1) }
106 let y = b.suffix(8).reduce(UInt64(0)) { $0 << 8 | UInt64($1) }
107 return (x > y ? x - y : y - x) <= boottimeTolerance
108 }
109
110 /// Sem identidade de boot, cada leitura parece um boot novo: a espera recomeca
111 /// cheia. Falha para o lado do dono esperar mais, nunca menos.
112 public static let unknownBoot = [UInt8](repeating: 0, count: 16)
113
114 /// O registro de "PIN digitado neste boot": a identidade do boot e o tempo desde o
115 /// boot naquele instante (24 bytes).
116 ///
117 /// So o instante do boot (`kern.boottime`) deixava uma brecha: depois de reiniciar,
118 /// quem acertasse a hora a mao para o boot antigo cair dentro da folga religava o Face
119 /// ID sem o PIN (auditoria 3, B2). O tempo desde o boot recomeca do zero ao reiniciar
120 /// e nao anda com o relogio: o registro so vale se o de agora for maior.
121 public static func pinRecord(boot: [UInt8], uptime: TimeInterval) -> [UInt8] {
122 boot + withUnsafeBytes(of: uptime.bitPattern.bigEndian) { Array($0) }
123 }
124
125 /// Registro antigo, de so 16 bytes, nao vale: o PIN e pedido uma vez.
126 public static func pinRecordValid(_ saved: [UInt8], boot: [UInt8], uptime: TimeInterval) -> Bool {
127 guard saved.count == 24 else { return false }
128 let recordedUptime = TimeInterval(bitPattern: saved.suffix(8).reduce(UInt64(0)) { $0 << 8 | UInt64($1) })
129 guard recordedUptime.isFinite, recordedUptime >= 0 else { return false }
130 return isSameBoot(Array(saved.prefix(16)), boot) && uptime >= recordedUptime
131 }
132}
133
134/// O registro de tentativas, gravado no chaveiro.
135///
136/// Formato 2 (29 bytes): versao, erros (u32), prazo no relogio monotonico (f64) e a
137/// identidade do boot em que o prazo foi gravado (16 bytes).
138struct AttemptRecord: Equatable {
139 var failures: UInt32
140 var uptimeDeadline: TimeInterval
141 var bootSession: [UInt8]
142
143 static let zero = AttemptRecord(failures: 0, uptimeDeadline: 0, bootSession: PINPolicy.unknownBoot)
144 private static let version: UInt8 = 2
145
146 var encoded: Data {
147 var out = Data([Self.version])
148 out.append(contentsOf: failures.bigEndianByteArray)
149 out.append(contentsOf: uptimeDeadline.bitPattern.bigEndianByteArray)
150 out.append(contentsOf: bootSession)
151 return out
152 }
153
154 init(failures: UInt32, uptimeDeadline: TimeInterval, bootSession: [UInt8]) {
155 self.failures = failures
156 self.uptimeDeadline = uptimeDeadline
157 self.bootSession = bootSession
158 }
159
160 init?(_ data: Data) {
161 let bytes = [UInt8](data)
162 switch bytes.count {
163 case 29 where bytes[0] == Self.version:
164 failures = bytes[1..<5].reduce(0) { $0 << 8 | UInt32($1) }
165 uptimeDeadline = Double(bitPattern: bytes[5..<13].reduce(0) { $0 << 8 | UInt64($1) })
166 bootSession = Array(bytes[13..<29])
167 case 28:
168 // Formato 1, de antes da auditoria: so o numero de erros ainda vale. O
169 // boot desconhecido faz a espera recomecar cheia no proximo acesso.
170 failures = bytes[0..<4].reduce(0) { $0 << 8 | UInt32($1) }
171 uptimeDeadline = 0
172 bootSession = PINPolicy.unknownBoot
173 default:
174 return nil
175 }
176 }
177
178 /// Registro do erro numero `failures`, com o prazo contado a partir de agora.
179 static func after(failures: UInt32) -> AttemptRecord {
180 AttemptRecord(
181 failures: failures,
182 uptimeDeadline: PINPolicy.uptime + PINPolicy.delay(afterFailures: failures),
183 bootSession: PINPolicy.bootSession
184 )
185 }
186
187 /// O prazo foi gravado em outro boot (ou num formato antigo)?
188 var isFromAnotherBoot: Bool {
189 failures > 0 && !PINPolicy.isSameBoot(bootSession, PINPolicy.bootSession)
190 }
191
192 /// Quanto falta de espera. De outro boot, a resposta e a espera cheia ate alguem
193 /// regravar o prazo (`RootKeyVault.rebaseAttempts`); ler nunca escreve.
194 func remaining() -> TimeInterval {
195 let full = PINPolicy.delay(afterFailures: failures)
196 guard !isFromAnotherBoot else { return full }
197 return min(full, max(0, uptimeDeadline - PINPolicy.uptime))
198 }
199}
200
201/// O prazo da espera depois que o Face ID caiu (`PINPolicy.biometryLossHold`), gravado no
202/// chaveiro. 25 bytes: versao, prazo no relogio monotonico (f64) e a identidade do boot.
203/// Mesmo relogio do contador de tentativas: mudar a hora nao encurta, e depois de
204/// reiniciar a espera recomeca cheia.
205struct HoldRecord: Equatable {
206 var uptimeDeadline: TimeInterval
207 var bootSession: [UInt8]
208
209 private static let version: UInt8 = 1
210
211 static func startingNow() -> HoldRecord {
212 HoldRecord(uptimeDeadline: PINPolicy.uptime + PINPolicy.biometryLossHold, bootSession: PINPolicy.bootSession)
213 }
214
215 var encoded: Data {
216 var out = Data([Self.version])
217 out.append(contentsOf: uptimeDeadline.bitPattern.bigEndianByteArray)
218 out.append(contentsOf: bootSession)
219 return out
220 }
221
222 init(uptimeDeadline: TimeInterval, bootSession: [UInt8]) {
223 self.uptimeDeadline = uptimeDeadline
224 self.bootSession = bootSession
225 }
226
227 init?(_ data: Data) {
228 let bytes = [UInt8](data)
229 guard bytes.count == 25, bytes[0] == Self.version else { return nil }
230 uptimeDeadline = Double(bitPattern: bytes[1..<9].reduce(0) { $0 << 8 | UInt64($1) })
231 bootSession = Array(bytes[9..<25])
232 }
233
234 var isFromAnotherBoot: Bool { !PINPolicy.isSameBoot(bootSession, PINPolicy.bootSession) }
235
236 func remaining() -> TimeInterval {
237 let full = PINPolicy.biometryLossHold
238 guard !isFromAnotherBoot, uptimeDeadline.isFinite else { return full }
239 return min(full, max(0, uptimeDeadline - PINPolicy.uptime))
240 }
241}