1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
// Author: cirakg & claude ai
// Summary: Authenticated encryption of records: AES-256-CBC plus HMAC-SHA256, encrypt-then-MAC (README 7.3).
//
// Format of a sealed box (one ByteArray):
// version (1 byte, = 1) | IV (16 bytes) | AES-256-CBC ciphertext | tag (32 bytes)
// tag = HMAC-SHA256(macKey, "OTPW-REC-1" || len(aad) as 4 bytes || aad || version || IV || ciphertext)
//
// * Two keys come from one master key with HKDF (labels "OTPW enc v1" and
// "OTPW mac v1"), so encryption and authentication never share a key.
// * aad ("additional authenticated data") is authenticated but not encrypted;
// storage puts the record ID and header there (M3), so a record can't be
// swapped for another one or have its header changed unnoticed. Its length
// is part of the MAC input, so aad and ciphertext can't be shifted against
// each other.
// * open() checks the tag in constant time BEFORE decrypting, and returns
// null for any problem (wrong key, tampering, truncation, unknown version)
// without saying which.
// * Every seal uses a fresh random IV from Cryptography.randomBytes.
import Toybox.Cryptography;
import Toybox.Lang;
// (:glance): also loaded by the glance, which shows the favourite code (GlanceCode).
(:glance)
module SealedBox {
const VERSION = 1;
const IV_BYTES = 16;
const TAG_BYTES = 32;
const KEY_BYTES = 32;
const MIN_SIZE = 1 + IV_BYTES + 16 + TAG_BYTES; // ciphertext is at least one block
// The encryption and MAC keys derived from one master key.
class Keys {
var encKey as ByteArray;
var macKey as ByteArray;
function initialize(masterKey as ByteArray) {
encKey = Hkdf.derive(masterKey, []b, Bytes.fromUtf8("OTPW enc v1"), KEY_BYTES);
macKey = Hkdf.derive(masterKey, []b, Bytes.fromUtf8("OTPW mac v1"), KEY_BYTES);
}
// Wipe both keys (when the session locks).
function wipe() as Void {
Bytes.wipe(encKey);
Bytes.wipe(macKey);
}
}
// Seal plaintext with a fresh random IV.
function seal(keys as Keys, aad as ByteArray, plaintext as ByteArray) as ByteArray {
return sealWithIv(keys, aad, plaintext, Cryptography.randomBytes(IV_BYTES));
}
// Seal with a given IV. Only tests call this directly (fixed IVs make the
// output reproducible); reusing an IV in real use would be a mistake.
function sealWithIv(keys as Keys, aad as ByteArray, plaintext as ByteArray, iv as ByteArray) as ByteArray {
return sealKeepWithIv(keys, aad, plaintext, iv, null);
}
// As seal(); with `leftovers`, the padded plaintext copy is added there
// for the caller to wipe in steps (zeroing 4 KB is a large part of a
// watchdog step) instead of wiped here.
function sealKeep(keys as Keys, aad as ByteArray, plaintext as ByteArray, leftovers as Array<ByteArray>?) as ByteArray {
return sealKeepWithIv(keys, aad, plaintext, Cryptography.randomBytes(IV_BYTES), leftovers);
}
function sealKeepWithIv(keys as Keys, aad as ByteArray, plaintext as ByteArray, iv as ByteArray,
leftovers as Array<ByteArray>?) as ByteArray {
var ciphertext = Aes.cbcEncryptKeep(keys.encKey, iv, plaintext, leftovers);
var box = [VERSION]b;
box.addAll(iv);
box.addAll(ciphertext);
box.addAll(tag(keys, aad, box));
return box;
}
// The plaintext, or null if the box is malformed, was sealed with another
// key or aad, or was changed in any way.
function open(keys as Keys, aad as ByteArray, box as ByteArray) as ByteArray? {
return openKeep(keys, aad, box, null);
}
// As open(); with `leftovers`, the padded plaintext is added there.
function openKeep(keys as Keys, aad as ByteArray, box as ByteArray, leftovers as Array<ByteArray>?) as ByteArray? {
var size = box.size();
if (size < MIN_SIZE || (size - 1 - IV_BYTES - TAG_BYTES) % Aes.BLOCK_BYTES != 0 || box[0] != VERSION) {
return null;
}
var body = box.slice(0, size - TAG_BYTES);
var expected = tag(keys, aad, body);
var valid = Bytes.equal(expected, box.slice(size - TAG_BYTES, size));
Bytes.wipe(expected);
if (!valid) {
return null;
}
return Aes.cbcDecryptKeep(keys.encKey, box.slice(1, 1 + IV_BYTES), box.slice(1 + IV_BYTES, size - TAG_BYTES), leftovers);
}
// HMAC over the label, the aad (with its length) and the box body.
function tag(keys as Keys, aad as ByteArray, body as ByteArray) as ByteArray {
var input = Bytes.fromUtf8("OTPW-REC-1");
input.addAll(Bytes.uint32BigEndian(aad.size()));
input.addAll(aad);
input.addAll(body);
return Hmac.nativeSha256(keys.macKey, input);
}
}
|