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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
// Author: cirakg & claude ai
// Summary: Tests for the TLV codec and the record format: round trips per entry type, strict decoding, tamper detection.
import Toybox.Lang;
import Toybox.Test;
(:test)
function codecRoundTripsEveryType(logger as Test.Logger) as Boolean {
var w = new Codec.Writer();
w.addBytes(1, [0, 255, 7]b);
w.addText(2, "Zürich ✓");
w.addText(3, "");
w.addInt(4, -2l);
w.addInt(5, 0x7FFFFFFFFFFFFFFFl);
w.addBool(6, true);
w.addBool(7, false);
var f = Codec.decode(w.bytes);
if (f == null) {
logger.error("decode failed");
return false;
}
return Bytes.equal(f[1] as ByteArray, [0, 255, 7]b)
&& (f[2] as String).equals("Zürich ✓") && (f[3] as String).equals("")
&& (f[4] as Long) == -2l && (f[5] as Long) == 0x7FFFFFFFFFFFFFFFl
&& (f[6] as Boolean) == true && (f[7] as Boolean) == false;
}
(:test)
function codecRejectsMalformedInput(logger as Test.Logger) as Boolean {
var bad = [
[1]b, // truncated field header
[1, 1, 0, 5, 1, 2]b, // value shorter than its length
[1, 9, 0, 0]b, // unknown type
[1, 3, 0, 4, 0, 0, 0, 0]b, // integer that isn't 8 bytes
[1, 4, 0, 1, 2]b, // boolean other than 0/1
[1, 1, 0, 0, 1, 1, 0, 0]b, // duplicate tag
[1, 2, 0, 2, 0xC3, 0x28]b // invalid UTF-8
];
for (var i = 0; i < bad.size(); i++) {
if (Codec.decode(bad[i] as ByteArray) != null) {
logger.error("malformed input " + i + " was accepted");
return false;
}
}
var empty = Codec.decode([]b);
return empty != null && empty.size() == 0;
}
(:test)
function recordRoundTripsEveryEntryType(logger as Test.Logger) as Boolean {
var keys = SealedBoxFixtures.keys(7);
var entries = [
StorageFixtures.totp("Example TOTP"),
StorageFixtures.hotp("Example HOTP", 4294967297l),
StorageFixtures.steam("Example Steam"),
StorageFixtures.code("Gym card")
];
for (var i = 0; i < entries.size(); i++) {
for (var names = 0; names < 2; names++) {
var e = entries[i] as Entry;
e.id = "K7F" + (i + 2);
e.namesEncrypted = names == 1;
var record = Record.seal(e, Record.KEY_DEVICE, keys);
var back = Record.open(record, e.id, keys);
if (back == null || !StorageFixtures.same(logger, back, e)) {
logger.error("round trip failed for entry " + i + ", namesEncrypted " + e.namesEncrypted);
return false;
}
var header = Record.decodeHeader(record);
if (header == null || header.secret != null || header.payload != null
|| header.name.equals(e.name) == e.namesEncrypted) {
logger.error("header of entry " + i + " exposes or loses data");
return false;
}
}
}
return true;
}
// The record's MAC covers the ID, the prefix and the header, so flipping any
// single byte of a record must make open() fail.
(:test)
function recordDetectsEveryTamperedByte(logger as Test.Logger) as Boolean {
var keys = SealedBoxFixtures.keys(8);
var e = StorageFixtures.hotp("Tamper", 5l);
e.id = "ABCD";
var record = Record.seal(e, Record.KEY_DEVICE, keys);
for (var i = 0; i < record.size(); i++) {
var tampered = Bytes.copy(record);
tampered[i] = tampered[i] ^ 0x01;
var opened = Record.open(tampered, "ABCD", keys);
if (opened != null) {
logger.error("tampering byte " + i + " of " + record.size() + " was not detected");
return false;
}
}
return true;
}
(:test)
function recordRejectsWrongIdKeyAndTruncation(logger as Test.Logger) as Boolean {
var keys = SealedBoxFixtures.keys(9);
var e = StorageFixtures.totp("Moved");
e.id = "WXYZ";
var record = Record.seal(e, Record.KEY_DEVICE, keys);
if (Record.open(record, "WXY2", keys) != null) {
logger.error("record opened under another ID");
return false;
}
if (Record.open(record, "WXYZ", SealedBoxFixtures.keys(10)) != null) {
logger.error("record opened with another key");
return false;
}
for (var cut = 0; cut < record.size(); cut += 3) {
if (Record.open(record.slice(0, cut), "WXYZ", keys) != null) {
logger.error("truncated record handled wrongly at " + cut);
return false;
}
}
return Record.keyRef(record) == Record.KEY_DEVICE;
}
(:test)
function entryValidationRejectsBadFields(logger as Test.Logger) as Boolean {
var cases = [] as Array<Entry>;
var e = StorageFixtures.totp("x"); e.digits = 9; cases.add(e);
e = StorageFixtures.totp("x"); e.period = 0; cases.add(e);
e = StorageFixtures.totp("x"); e.algorithm = 7; cases.add(e);
e = StorageFixtures.totp("x"); e.secret = []b; cases.add(e);
e = StorageFixtures.totp("x"); e.id = "abcd"; cases.add(e);
e = StorageFixtures.totp("x"); e.id = "ABC0"; cases.add(e);
e = StorageFixtures.totp("x"); e.name = "0123456789012345678901234567890123456789012345678901234567890123456789"; cases.add(e);
e = StorageFixtures.totp("x"); e.type = 5; cases.add(e);
e = StorageFixtures.code("x"); e.payload = null; cases.add(e);
e = StorageFixtures.code("x"); e.kind = 4; cases.add(e);
for (var i = 0; i < cases.size(); i++) {
if (cases[i].validate() == null) {
logger.error("invalid case " + i + " passed validation");
return false;
}
}
return StorageFixtures.totp("ok").validate() == null && StorageFixtures.code("ok").validate() == null
&& Entry.isValidId("K7F2") && !Entry.isValidId("K7F") && !Entry.isValidId("K7FI");
}
(:test)
function entryWipeZeroesSecretBuffers(logger as Test.Logger) as Boolean {
var e = StorageFixtures.totp("wipe");
var secret = e.secret as ByteArray;
var c = StorageFixtures.code("wipe");
var payload = c.payload as ByteArray;
e.wipeSecrets();
c.wipeSecrets();
return StorageFixtures.allZero(secret) && StorageFixtures.allZero(payload) && e.secret == null && c.payload == null;
}
// Prints the record sizes that docs/SECURITY.md quotes and the entry cap is
// based on.
(:test)
function recordSizesAreSmall(logger as Test.Logger) as Boolean {
var keys = SealedBoxFixtures.keys(11);
var e = StorageFixtures.totp("GitHub");
e.id = "K7F2";
var otpSize = Record.seal(e, Record.KEY_DEVICE, keys).size();
var c = StorageFixtures.code("Library card");
c.id = "K7F3";
var codeSize = Record.seal(c, Record.KEY_DEVICE, keys).size();
var big = StorageFixtures.code("Big");
big.id = "K7F4";
big.payload = new [Entry.MAX_PAYLOAD_BYTES]b;
var bigSize = Record.seal(big, Record.KEY_DEVICE, keys).size();
logger.debug("SIZE otp=" + otpSize + " code=" + codeSize + " max_payload=" + bigSize);
return otpSize < 300 && codeSize < 300 && bigSize < Record.MAX_BYTES;
}
|