sqrl-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,624 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SQRL
4
+ # The S4 ("SQRL Secure Storage System") identity file format.
5
+ #
6
+ # An identity file starts with the 8 byte header +sqrldata+ (binary) or +SQRLDATA+
7
+ # followed by base64url text, and then a sequence of length-prefixed blocks:
8
+ #
9
+ # * Type 1 (125 bytes): the password-protected IMK and ILK plus the user's settings.
10
+ # AES-256-GCM, key = EnScrypt(password, salt, logN, iterations), IV from the block,
11
+ # AAD = the 45 plaintext bytes of the block.
12
+ # * Type 2 (73 bytes): the rescue-code-protected IUK. AES-256-GCM, key =
13
+ # EnScrypt(rescue code digits, salt, logN, iterations), IV = 12 zero bytes,
14
+ # AAD = the 25 plaintext bytes of the block.
15
+ # * Type 3 (54/86/118/150 bytes): up to four previous IUKs. AES-256-GCM, key = IMK,
16
+ # IV = 12 zero bytes, AAD = the 6 plaintext bytes of the block.
17
+ #
18
+ # Unknown block types are preserved verbatim so that files written by other clients
19
+ # survive a round trip.
20
+ module Storage
21
+ # Binary file header.
22
+ HEADER_BINARY = "sqrldata".b.freeze
23
+ # Header of the base64url textual representation.
24
+ HEADER_TEXT = "SQRLDATA".b.freeze
25
+
26
+ # Type 1 option flag: the client should check for updates.
27
+ FLAG_CHECK_FOR_UPDATES = 0x0001
28
+ # Type 1 option flag: the client may update itself automatically.
29
+ FLAG_UPDATE_AUTOMATICALLY = 0x0002
30
+ # Type 1 option flag: request that sites disable non-SQRL authentication (+sqrlonly+).
31
+ FLAG_SQRL_ONLY = 0x0004
32
+ # Type 1 option flag: request that sites disable account recovery by other means (+hardlock+).
33
+ FLAG_NO_BYPASS = 0x0008
34
+ # Type 1 option flag: warn of a possible MITM attack when the server reports that the
35
+ # query's IP does not match the IP that requested the SQRL URL.
36
+ FLAG_WARN_MITM = 0x0010
37
+ # Type 1 option flag: discard the QuickPass when the system blanks, sleeps or suspends.
38
+ FLAG_CLEAR_QUICKPASS_ON_SLEEP = 0x0020
39
+ # Type 1 option flag: discard the QuickPass when the user is switched.
40
+ FLAG_CLEAR_QUICKPASS_ON_SWITCH = 0x0040
41
+ # Type 1 option flag: discard the QuickPass after the idle timeout (minutes) elapses.
42
+ FLAG_CLEAR_QUICKPASS_ON_IDLE = 0x0080
43
+ # Type 1 option flag: warn whenever a non-CPS authentication is attempted.
44
+ FLAG_WARN_NON_CPS = 0x0100
45
+ # Default option flags for a newly created identity (0x01F3, per the S4 specification).
46
+ DEFAULT_OPTION_FLAGS = FLAG_CHECK_FOR_UPDATES |
47
+ FLAG_UPDATE_AUTOMATICALLY |
48
+ FLAG_WARN_MITM |
49
+ FLAG_CLEAR_QUICKPASS_ON_SLEEP |
50
+ FLAG_CLEAR_QUICKPASS_ON_SWITCH |
51
+ FLAG_CLEAR_QUICKPASS_ON_IDLE |
52
+ FLAG_WARN_NON_CPS
53
+
54
+ # Maximum number of previous IUKs stored in a type 3 block.
55
+ MAX_PREVIOUS_KEYS = 4
56
+
57
+ ZERO_IV = ("\0" * 12).b.freeze
58
+ TYPE1_LEN = 125
59
+ TYPE1_PT_LEN = 45
60
+ TYPE2_LEN = 73
61
+ TYPE2_PT_LEN = 25
62
+ TYPE3_PT_LEN = 6
63
+ private_constant :ZERO_IV, :TYPE1_LEN, :TYPE1_PT_LEN, :TYPE2_LEN, :TYPE2_PT_LEN, :TYPE3_PT_LEN
64
+
65
+ # Normalize user-typed secret text the way all SQRL clients must before EnScrypt:
66
+ # Unicode NFKC composition, then UTF-8 (SQRL Cryptography, "Processing User Input").
67
+ def self.normalize_password(password)
68
+ password.to_s.encode("UTF-8").unicode_normalize(:nfkc).b
69
+ end
70
+
71
+ # Generate a new 24-digit rescue code from 256 bits of entropy.
72
+ def self.generate_rescue_code
73
+ rescue_code_from_entropy(Util.random_bytes(32))
74
+ end
75
+
76
+ # Deterministic core of .generate_rescue_code: digits are extracted by repeated long
77
+ # division of the 256-bit big-endian number by 10 (the code is +entropy mod 10**24+,
78
+ # which is uniform to within 2**-176).
79
+ def self.rescue_code_from_entropy(entropy)
80
+ raise ArgumentError, "entropy must be 32 bytes" unless entropy.bytesize == 32
81
+
82
+ num = entropy.bytes
83
+ digits = Array.new(24) do
84
+ rem = 0
85
+ num.map! do |byte|
86
+ cur = (rem << 8) | byte
87
+ rem = cur % 10
88
+ cur / 10
89
+ end
90
+ rem
91
+ end
92
+ digits.join
93
+ end
94
+
95
+ # Strip separators and whitespace from a rescue code and verify it has 24 digits.
96
+ def self.normalize_rescue_code(code)
97
+ code = code.to_s
98
+ digits = code.delete("^0-9")
99
+ non_digits = code.match?(/[^0-9\s-]/)
100
+ raise FormatError, "a rescue code is 24 decimal digits" if digits.length != 24 || non_digits
101
+
102
+ digits
103
+ end
104
+
105
+ # Format a rescue code as six groups of four digits separated by dashes.
106
+ def self.format_rescue_code(code)
107
+ code.to_s.scan(/.{1,4}/).join("-")
108
+ end
109
+
110
+ # Strip leading and trailing ASCII whitespace from a binary string.
111
+ def self.trim_ascii(bytes)
112
+ bytes.b.sub(/\A[ \t\r\n\f\v]+/, "").sub(/[ \t\r\n\f\v]+\z/, "")
113
+ end
114
+
115
+ # User-adjustable settings stored in the plaintext part of the type 1 block.
116
+ IdentitySettings = Struct.new(:option_flags, :hint_length, :pw_verify_seconds,
117
+ :idle_timeout_minutes, :log_n, keyword_init: true) do
118
+ def initialize(option_flags: DEFAULT_OPTION_FLAGS, hint_length: 4, pw_verify_seconds: 5,
119
+ idle_timeout_minutes: 15, log_n: Crypto::DEFAULT_LOG_N)
120
+ super
121
+ end
122
+ end
123
+
124
+ # Type 1 block: password-encrypted IMK and ILK.
125
+ class Type1Block
126
+ attr_accessor :iv, :salt, :settings, :iterations, :enc_imk, :enc_ilk, :tag
127
+
128
+ def initialize(iv:, salt:, settings:, iterations:, enc_imk:, enc_ilk:, tag:)
129
+ @iv = iv.b
130
+ @salt = salt.b
131
+ @settings = settings
132
+ @iterations = iterations
133
+ @enc_imk = enc_imk.b
134
+ @enc_ilk = enc_ilk.b
135
+ @tag = tag.b
136
+ end
137
+
138
+ # The 45 plaintext bytes (which double as the AES-GCM additional authenticated data).
139
+ def plaintext
140
+ [TYPE1_LEN, 1, TYPE1_PT_LEN].pack("vvv") + @iv + @salt +
141
+ [@settings.log_n, @iterations, @settings.option_flags, @settings.hint_length,
142
+ @settings.pw_verify_seconds, @settings.idle_timeout_minutes].pack("CVvCCv")
143
+ end
144
+
145
+ # Serialize the full 125-byte block.
146
+ def to_bytes
147
+ plaintext + @enc_imk + @enc_ilk + @tag
148
+ end
149
+
150
+ def self.parse(b)
151
+ raise FormatError, "type 1 block must be #{TYPE1_LEN} bytes, got #{b.bytesize}" unless b.bytesize == TYPE1_LEN
152
+
153
+ pt_len = Util.le_u16(b, 4)
154
+ raise FormatError, "unsupported type 1 plaintext length #{pt_len}" unless pt_len == TYPE1_PT_LEN
155
+
156
+ new(
157
+ iv: b.byteslice(6, 12),
158
+ salt: b.byteslice(18, 16),
159
+ settings: IdentitySettings.new(
160
+ log_n: b.getbyte(34),
161
+ option_flags: Util.le_u16(b, 39),
162
+ hint_length: b.getbyte(41),
163
+ pw_verify_seconds: b.getbyte(42),
164
+ idle_timeout_minutes: Util.le_u16(b, 43)
165
+ ),
166
+ iterations: Util.le_u32(b, 35),
167
+ enc_imk: b.byteslice(45, 32),
168
+ enc_ilk: b.byteslice(77, 32),
169
+ tag: b.byteslice(109, 16)
170
+ )
171
+ end
172
+
173
+ # Derive the AES key from the password (slow: runs EnScrypt for +iterations+). The
174
+ # block, if given, receives the number of completed iterations.
175
+ def derive_key(password, &)
176
+ pw = Storage.normalize_password(password)
177
+ key = Crypto.enscrypt_iterations(pw, @salt, @settings.log_n, @iterations, &)
178
+ Util.wipe(pw)
179
+ key
180
+ end
181
+
182
+ # Decrypt IMK and ILK with an already-derived key. Returns +[imk, ilk]+.
183
+ def decrypt_with_key(key)
184
+ pt = Crypto.aes_gcm_decrypt(key, @iv, plaintext, @enc_imk + @enc_ilk + @tag)
185
+ [pt.byteslice(0, 32), pt.byteslice(32, 32)]
186
+ rescue CryptoError
187
+ raise CryptoError, "incorrect password"
188
+ end
189
+
190
+ # Build a fresh block encrypting +imk+/+ilk+ under +password+, running EnScrypt for
191
+ # +settings.pw_verify_seconds+ (as measured by +now_ms+). The block, if given,
192
+ # receives +(elapsed_ms, duration_ms)+.
193
+ def self.encrypt(imk, ilk, password, settings, now_ms: Crypto.method(:monotonic_ms), &progress)
194
+ salt = Util.random_bytes(16)
195
+ iv = Util.random_bytes(12)
196
+ duration_ms = [settings.pw_verify_seconds, 1].max * 1000
197
+ pw = Storage.normalize_password(password)
198
+ key, iterations = Crypto.enscrypt_duration(pw, salt, settings.log_n, duration_ms, now_ms: now_ms, &progress)
199
+ Util.wipe(pw)
200
+ block = encrypt_with_key(imk, ilk, key, salt, iv, iterations, settings)
201
+ Util.wipe(key)
202
+ block
203
+ end
204
+
205
+ # Build a block with a pre-derived key (used by tests and for deterministic rebuilds).
206
+ def self.encrypt_with_key(imk, ilk, key, salt, iv, iterations, settings)
207
+ block = new(iv: iv, salt: salt, settings: settings, iterations: iterations,
208
+ enc_imk: "\0" * 32, enc_ilk: "\0" * 32, tag: "\0" * 16)
209
+ ct = Crypto.aes_gcm_encrypt(key, block.iv, block.plaintext, imk.b + ilk.b)
210
+ block.enc_imk = ct.byteslice(0, 32)
211
+ block.enc_ilk = ct.byteslice(32, 32)
212
+ block.tag = ct.byteslice(64, 16)
213
+ block
214
+ end
215
+
216
+ # @return [Boolean] whether +other+ carries the same values
217
+ def ==(other)
218
+ other.is_a?(Type1Block) && to_bytes == other.to_bytes
219
+ end
220
+ alias eql? ==
221
+
222
+ # @return [Integer] hash consistent with {#==}
223
+ def hash
224
+ to_bytes.hash
225
+ end
226
+ end
227
+
228
+ # Type 2 block: rescue-code-encrypted IUK.
229
+ class Type2Block
230
+ attr_accessor :salt, :log_n, :iterations, :enc_iuk, :tag
231
+
232
+ def initialize(salt:, log_n:, iterations:, enc_iuk:, tag:)
233
+ @salt = salt.b
234
+ @log_n = log_n
235
+ @iterations = iterations
236
+ @enc_iuk = enc_iuk.b
237
+ @tag = tag.b
238
+ end
239
+
240
+ # The 25 plaintext bytes (also the AAD).
241
+ def plaintext
242
+ [TYPE2_LEN, 2].pack("vv") + @salt + [@log_n, @iterations].pack("CV")
243
+ end
244
+
245
+ # Serialize the full 73-byte block.
246
+ def to_bytes
247
+ plaintext + @enc_iuk + @tag
248
+ end
249
+
250
+ def self.parse(b)
251
+ raise FormatError, "type 2 block must be #{TYPE2_LEN} bytes, got #{b.bytesize}" unless b.bytesize == TYPE2_LEN
252
+
253
+ new(salt: b.byteslice(4, 16), log_n: b.getbyte(20), iterations: Util.le_u32(b, 21),
254
+ enc_iuk: b.byteslice(25, 32), tag: b.byteslice(57, 16))
255
+ end
256
+
257
+ # Decrypt the IUK with the rescue code (slow: runs EnScrypt for +iterations+).
258
+ def decrypt(rescue_code, &)
259
+ digits = Storage.normalize_rescue_code(rescue_code)
260
+ key = Crypto.enscrypt_iterations(digits, @salt, @log_n, @iterations, &)
261
+ begin
262
+ Crypto.aes_gcm_decrypt(key, ZERO_IV, plaintext, @enc_iuk + @tag)
263
+ rescue CryptoError
264
+ raise CryptoError, "incorrect rescue code"
265
+ ensure
266
+ Util.wipe(key)
267
+ end
268
+ end
269
+
270
+ # Encrypt an IUK under a rescue code, running EnScrypt for +duration_ms+.
271
+ def self.encrypt(iuk, rescue_code, log_n, duration_ms, now_ms: Crypto.method(:monotonic_ms), &progress)
272
+ digits = Storage.normalize_rescue_code(rescue_code)
273
+ salt = Util.random_bytes(16)
274
+ key, iterations = Crypto.enscrypt_duration(digits, salt, log_n, duration_ms, now_ms: now_ms, &progress)
275
+ block = encrypt_with_key(iuk, key, salt, log_n, iterations)
276
+ Util.wipe(key)
277
+ block
278
+ end
279
+
280
+ # Encrypt with a pre-derived key.
281
+ def self.encrypt_with_key(iuk, key, salt, log_n, iterations)
282
+ block = new(salt: salt, log_n: log_n, iterations: iterations, enc_iuk: "\0" * 32, tag: "\0" * 16)
283
+ ct = Crypto.aes_gcm_encrypt(key, ZERO_IV, block.plaintext, iuk.b)
284
+ block.enc_iuk = ct.byteslice(0, 32)
285
+ block.tag = ct.byteslice(32, 16)
286
+ block
287
+ end
288
+
289
+ # @return [Boolean] whether +other+ carries the same values
290
+ def ==(other)
291
+ other.is_a?(Type2Block) && to_bytes == other.to_bytes
292
+ end
293
+ alias eql? ==
294
+
295
+ # @return [Integer] hash consistent with {#==}
296
+ def hash
297
+ to_bytes.hash
298
+ end
299
+ end
300
+
301
+ # Type 3 block: previous IUKs encrypted under the current IMK.
302
+ class Type3Block
303
+ # The identity's "edition": a counter incremented once per re-keying. It is part of the
304
+ # authenticated plaintext, so it can be shown without the password. The number of
305
+ # stored keys is given by the block length, not by this field.
306
+ attr_accessor :edition
307
+ # Encrypted previous IUKs, most recent first (1 to 4 entries).
308
+ attr_accessor :enc_keys
309
+ attr_accessor :tag
310
+
311
+ def initialize(edition:, enc_keys:, tag:)
312
+ @edition = edition
313
+ @enc_keys = enc_keys.map(&:b)
314
+ @tag = tag.b
315
+ end
316
+
317
+ # Total block length for this many keys.
318
+ def length
319
+ TYPE3_PT_LEN + (32 * @enc_keys.length) + 16
320
+ end
321
+ alias len length
322
+
323
+ # Whether the block holds no keys (never true for a well-formed block).
324
+ def empty?
325
+ @enc_keys.empty?
326
+ end
327
+
328
+ # The 6 plaintext bytes (also the AAD).
329
+ def plaintext
330
+ [length, 3, @edition].pack("vvv")
331
+ end
332
+
333
+ # @return [String] the block serialised, header included
334
+ def to_bytes
335
+ plaintext + @enc_keys.join + @tag
336
+ end
337
+
338
+ def self.parse(b)
339
+ raise FormatError, "type 3 block too short" if b.bytesize < TYPE3_PT_LEN + 32 + 16
340
+
341
+ edition = Util.le_u16(b, 4)
342
+ raise FormatError, "type 3 block has edition 0" if edition.zero?
343
+
344
+ body = b.bytesize - TYPE3_PT_LEN - 16
345
+ if (body % 32) != 0 || body.zero? || body / 32 > MAX_PREVIOUS_KEYS
346
+ raise FormatError, "type 3 block length #{b.bytesize} is not 54, 86, 118 or 150"
347
+ end
348
+
349
+ count = body / 32
350
+ enc_keys = Array.new(count) { |i| b.byteslice(TYPE3_PT_LEN + (32 * i), 32) }
351
+ new(edition: edition, enc_keys: enc_keys, tag: b.byteslice(-16, 16))
352
+ end
353
+
354
+ # Decrypt the previous IUKs with the current IMK.
355
+ def decrypt(imk)
356
+ pt = Crypto.aes_gcm_decrypt(imk, ZERO_IV, plaintext, @enc_keys.join + @tag)
357
+ pt.bytes.each_slice(32).map { |c| c.pack("C*") }
358
+ rescue CryptoError
359
+ raise CryptoError, "previous identity block does not verify"
360
+ end
361
+
362
+ # Encrypt previous IUKs (most recent first; at most four are kept) under +imk+ with the
363
+ # given edition number. Returns nil when there are no keys.
364
+ def self.encrypt(previous_iuks, imk, edition)
365
+ keys = previous_iuks.first(MAX_PREVIOUS_KEYS)
366
+ return nil if keys.empty?
367
+
368
+ block = new(edition: [edition, 1].max, enc_keys: Array.new(keys.length) { "\0" * 32 }, tag: "\0" * 16)
369
+ ct = Crypto.aes_gcm_encrypt(imk, ZERO_IV, block.plaintext, keys.map(&:b).join)
370
+ block.enc_keys = Array.new(keys.length) { |i| ct.byteslice(32 * i, 32) }
371
+ block.tag = ct.byteslice(32 * keys.length, 16)
372
+ block
373
+ end
374
+ # @return [Boolean] whether +other+ carries the same values
375
+
376
+ def ==(other)
377
+ other.is_a?(Type3Block) && to_bytes == other.to_bytes
378
+ end
379
+ alias eql? ==
380
+ # @return [Integer] hash consistent with {#==}
381
+
382
+ def hash
383
+ to_bytes.hash
384
+ end
385
+ end
386
+ end
387
+
388
+ # A parsed S4 identity file.
389
+ class S4Identity
390
+ include Storage
391
+
392
+ # Password block (Storage::Type1Block or nil).
393
+ attr_accessor :type1
394
+ # Rescue code block (Storage::Type2Block or nil).
395
+ attr_accessor :type2
396
+ # Previous identities block (Storage::Type3Block or nil).
397
+ attr_accessor :type3
398
+ # Blocks of unknown type, preserved verbatim (each including its length/type prefix).
399
+ attr_accessor :unknown
400
+
401
+ def initialize(type1: nil, type2: nil, type3: nil, unknown: [])
402
+ @type1 = type1
403
+ @type2 = type2
404
+ @type3 = type3
405
+ @unknown = unknown.map(&:b)
406
+ end
407
+
408
+ # Parse either the binary (+sqrldata+) or the textual base64url (+SQRLDATA+) form.
409
+ def self.parse(input)
410
+ trimmed = Storage.trim_ascii(input)
411
+ raise FormatError, "identity data too short" if trimmed.bytesize < 8
412
+
413
+ header = trimmed.byteslice(0, 8)
414
+ if header == HEADER_TEXT
415
+ text = trimmed.byteslice(8..)
416
+ raise FormatError, "invalid SQRLDATA text" unless text.force_encoding("UTF-8").valid_encoding?
417
+
418
+ return parse_blocks(Util.b64u_decode(text))
419
+ end
420
+ return parse_blocks(trimmed.byteslice(8..)) if header == HEADER_BINARY
421
+
422
+ raise FormatError, "missing sqrldata header"
423
+ end
424
+
425
+ # Parse a sequence of blocks without the file header (used for the textual identity too).
426
+ def self.parse_blocks(data)
427
+ data = data.b
428
+ id = new
429
+ while data.bytesize >= 4
430
+ len = Util.le_u16(data, 0)
431
+ typ = Util.le_u16(data, 2)
432
+ if len.zero?
433
+ # zero padding (textual identities may carry trailing zero bytes)
434
+ break if data.each_byte.all?(&:zero?)
435
+
436
+ raise FormatError, "zero-length block"
437
+ end
438
+ if len < 4 || len > data.bytesize
439
+ raise FormatError, "block of type #{typ} claims #{len} bytes but only #{data.bytesize} remain"
440
+ end
441
+
442
+ block = data.byteslice(0, len)
443
+ # "Any client encountering an identity containing more than one of either type
444
+ # should reject the entire identity as suspicious and invalid."
445
+ duplicate = case typ
446
+ when 1 then !id.type1.nil?
447
+ when 2 then !id.type2.nil?
448
+ when 3 then !id.type3.nil?
449
+ else false
450
+ end
451
+ raise FormatError, "identity contains more than one type #{typ} block" if duplicate
452
+
453
+ case typ
454
+ when 1 then id.type1 = Type1Block.parse(block)
455
+ when 2 then id.type2 = Type2Block.parse(block)
456
+ when 3 then id.type3 = Type3Block.parse(block)
457
+ else id.unknown << block
458
+ end
459
+ data = data.byteslice(len..)
460
+ end
461
+ raise FormatError, "trailing garbage after last block" if data.each_byte.any? { |b| b != 0 }
462
+ if id.type1.nil? && id.type2.nil?
463
+ raise FormatError, "identity contains neither a password nor a rescue block"
464
+ end
465
+
466
+ id
467
+ end
468
+
469
+ # Serialize the blocks (type 1, 2, 3, then unknown) without a header.
470
+ def blocks_to_bytes
471
+ out = +"".b
472
+ out << @type1.to_bytes if @type1
473
+ out << @type2.to_bytes if @type2
474
+ out << @type3.to_bytes if @type3
475
+ @unknown.each { |u| out << u }
476
+ out
477
+ end
478
+
479
+ # The type 2 + type 3 blocks only: what the textual identity and the rescue-only export
480
+ # contain. (Type 1 is omitted because it is tied to the password of this installation.)
481
+ def rescue_blocks_to_bytes
482
+ out = +"".b
483
+ out << @type2.to_bytes if @type2
484
+ out << @type3.to_bytes if @type3
485
+ out
486
+ end
487
+
488
+ # Binary +sqrldata+ file.
489
+ def to_bytes
490
+ HEADER_BINARY + blocks_to_bytes
491
+ end
492
+
493
+ # Textual +SQRLDATA+ + base64url representation.
494
+ def to_text
495
+ "SQRLDATA#{Util.b64u_encode(blocks_to_bytes)}"
496
+ end
497
+
498
+ # A copy containing only the rescue (type 2) and previous (type 3) blocks.
499
+ def rescue_only
500
+ S4Identity.new(type2: @type2, type3: @type3)
501
+ end
502
+
503
+ # Settings from the type 1 block, or defaults.
504
+ def settings
505
+ @type1 ? @type1.settings.dup : IdentitySettings.new
506
+ end
507
+
508
+ # The identity's edition: 0 for a never re-keyed identity, else the type 3 counter.
509
+ def edition
510
+ @type3 ? @type3.edition : 0
511
+ end
512
+
513
+ def password_block?
514
+ !@type1.nil?
515
+ end
516
+
517
+ def rescue_block?
518
+ !@type2.nil?
519
+ end
520
+
521
+ # Derive the password key (slow). The block receives completed EnScrypt iterations out
522
+ # of +type1.iterations+.
523
+ def derive_password_key(password, &)
524
+ raise FormatError, "identity has no password block" unless @type1
525
+
526
+ @type1.derive_key(password, &)
527
+ end
528
+
529
+ # Unlock the identity with an already-derived password key.
530
+ def unlock_with_key(key)
531
+ raise FormatError, "identity has no password block" unless @type1
532
+
533
+ imk, ilk = @type1.decrypt_with_key(key)
534
+ previous = @type3 ? @type3.decrypt(imk) : []
535
+ UnlockedIdentity.new(imk, ilk, previous)
536
+ end
537
+
538
+ # Unlock the identity with the password (slow).
539
+ def unlock(password, &)
540
+ key = derive_password_key(password, &)
541
+ unlock_with_key(key)
542
+ ensure
543
+ Util.wipe(key) if key
544
+ end
545
+
546
+ # Decrypt the IUK with the rescue code (slow).
547
+ def unlock_iuk(rescue_code, &)
548
+ raise FormatError, "identity has no rescue code block" unless @type2
549
+
550
+ @type2.decrypt(rescue_code, &)
551
+ end
552
+
553
+ # Create a complete new identity file from an IUK.
554
+ #
555
+ # +rescue_duration_ms+ controls how long EnScrypt runs for the rescue block (the
556
+ # reference client uses a long time because the rescue code is only 24 digits). The
557
+ # block, if given, receives +(stage, elapsed_ms, duration_ms)+ with stage "password" or
558
+ # "rescue".
559
+ def self.create(iuk, password, rescue_code, settings = IdentitySettings.new, rescue_duration_ms = 5000,
560
+ now_ms: Crypto.method(:monotonic_ms), &progress)
561
+ imk = Identity.imk_from_iuk(iuk)
562
+ ilk = Identity.ilk_from_iuk(iuk)
563
+ type1 = Type1Block.encrypt(imk, ilk, password, settings, now_ms: now_ms) do |a, b|
564
+ progress&.call("password", a, b)
565
+ end
566
+ type2 = Type2Block.encrypt(iuk, rescue_code, settings.log_n, rescue_duration_ms, now_ms: now_ms) do |a, b|
567
+ progress&.call("rescue", a, b)
568
+ end
569
+ Util.wipe(imk)
570
+ Util.wipe(ilk)
571
+ new(type1: type1, type2: type2)
572
+ end
573
+
574
+ # Re-encrypt the type 1 (and type 3) blocks for an unlocked identity under a new
575
+ # password / settings. Used for password changes, settings changes and after importing a
576
+ # rescue-only identity. The block receives +(elapsed_ms, duration_ms)+.
577
+ def set_password(identity, password, settings = self.settings, now_ms: Crypto.method(:monotonic_ms), &progress)
578
+ @type1 = Type1Block.encrypt(identity.imk, identity.ilk, password, settings, now_ms: now_ms, &progress)
579
+ edition = [self.edition, identity.previous_iuks.length].max
580
+ @type3 = Type3Block.encrypt(identity.previous_iuks, identity.imk, edition)
581
+ self
582
+ end
583
+
584
+ # Re-key the identity: a new IUK is supplied, the old IUK (recovered with the old rescue
585
+ # code) moves to the front of the previous-identity list, and new type 1 / 2 / 3 blocks
586
+ # are produced for +new_password+ and +new_rescue_code+.
587
+ #
588
+ # Returns the new unlocked identity (with the previous IUKs).
589
+ def rekey(old_identity, old_iuk, new_iuk, new_password, new_rescue_code, settings = self.settings,
590
+ rescue_duration_ms = 5000, now_ms: Crypto.method(:monotonic_ms), &progress)
591
+ previous = [old_iuk.b] + old_identity.previous_iuks.first(MAX_PREVIOUS_KEYS - 1)
592
+ new_identity = UnlockedIdentity.from_iuk(new_iuk, previous)
593
+ type1 = Type1Block.encrypt(new_identity.imk, new_identity.ilk, new_password, settings, now_ms: now_ms) do |a, b|
594
+ progress&.call("password", a, b)
595
+ end
596
+ type2 = Type2Block.encrypt(new_iuk, new_rescue_code, settings.log_n, rescue_duration_ms, now_ms: now_ms) do |a, b|
597
+ progress&.call("rescue", a, b)
598
+ end
599
+ edition = [self.edition + 1, 0xFFFF].min
600
+ @type1 = type1
601
+ @type2 = type2
602
+ @type3 = Type3Block.encrypt(new_identity.previous_iuks, new_identity.imk, edition)
603
+ new_identity
604
+ # @return [Boolean] whether +other+ carries the same values
605
+ end
606
+
607
+ # @return [Boolean] whether +other+ carries the same values
608
+ def ==(other)
609
+ other.is_a?(S4Identity) && to_bytes == other.to_bytes
610
+ end
611
+ # @return [Integer] hash consistent with {#==}
612
+ alias eql? ==
613
+
614
+ # @return [Integer] hash consistent with {#==}
615
+ def hash
616
+ to_bytes.hash
617
+ end
618
+
619
+ # @return [String] a short description with secret material redacted
620
+ def inspect
621
+ "#<SQRL::S4Identity type1=#{!@type1.nil?} type2=#{!@type2.nil?} edition=#{edition} unknown=#{@unknown.length}>"
622
+ end
623
+ end
624
+ end