summaryrefslogtreecommitdiff
path: root/source/util/Bytes.mc
blob: a32475a1bd6414ddb46047ea5d0fa3cf5d092e4f (plain) (blame)
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: Byte-array helpers: hex and UTF-8 conversion, concatenation, wiping and constant-time comparison.
//
// Security notes:
//   * wipe() overwrites a buffer in place. Call it on every buffer that held a
//     key or a decrypted secret as soon as it is no longer needed. Monkey C
//     can't guarantee no other copy exists (README 7.8), so this is best effort.
//   * equal() always looks at every byte, so comparing a MAC doesn't reveal
//     through its running time how many leading bytes matched.

import Toybox.Lang;
import Toybox.StringUtil;

module Bytes {

    // Hex text (any case, even length) to bytes. Empty text gives an empty array.
    function fromHex(hex as String) as ByteArray {
        if (hex.length() == 0) {
            return []b;
        }
        return StringUtil.convertEncodedString(hex, {
            :fromRepresentation => StringUtil.REPRESENTATION_STRING_HEX,
            :toRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY
        }) as ByteArray;
    }

    // Bytes to lowercase hex text.
    function toHex(data as ByteArray) as String {
        if (data.size() == 0) {
            return "";
        }
        return (StringUtil.convertEncodedString(data, {
            :fromRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY,
            :toRepresentation => StringUtil.REPRESENTATION_STRING_HEX
        }) as String).toLower();
    }

    // The UTF-8 bytes of a string.
    function fromUtf8(text as String) as ByteArray {
        if (text.length() == 0) {
            return []b;
        }
        return StringUtil.convertEncodedString(text, {
            :fromRepresentation => StringUtil.REPRESENTATION_STRING_PLAIN_TEXT,
            :toRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY,
            :encoding => StringUtil.CHAR_ENCODING_UTF8
        }) as ByteArray;
    }

    // Decode UTF-8 bytes to a string. Empty input gives "". Malformed UTF-8
    // or a NUL byte gives null (callers treat that as damaged data). The
    // bytes are checked first because convertEncodedString stops the app
    // with an uncatchable system error on invalid UTF-8.
    function toUtf8(data as ByteArray) as String? {
        if (data.size() == 0) {
            return "";
        }
        if (!isValidUtf8(data)) {
            return null;
        }
        return StringUtil.convertEncodedString(data, {
            :fromRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY,
            :toRepresentation => StringUtil.REPRESENTATION_STRING_PLAIN_TEXT,
            :encoding => StringUtil.CHAR_ENCODING_UTF8
        }) as String;
    }

    // True for well-formed UTF-8 (RFC 3629) without NUL bytes: no overlong
    // forms, no surrogates (U+D800-DFFF), nothing above U+10FFFF.
    function isValidUtf8(data as ByteArray) as Boolean {
        var i = 0;
        var n = data.size();
        while (i < n) {
            var b = data[i];
            var more = 0;
            var min = 0;
            var cp = 0;
            if (b == 0) {
                return false;
            } else if (b < 0x80) {
                i++;
                continue;
            } else if (b >= 0xC2 && b <= 0xDF) {
                more = 1; min = 0x80; cp = b & 0x1F;
            } else if (b >= 0xE0 && b <= 0xEF) {
                more = 2; min = 0x800; cp = b & 0x0F;
            } else if (b >= 0xF0 && b <= 0xF4) {
                more = 3; min = 0x10000; cp = b & 0x07;
            } else {
                return false;
            }
            if (i + more >= n) {
                return false;
            }
            for (var k = 1; k <= more; k++) {
                var c = data[i + k];
                if ((c & 0xC0) != 0x80) {
                    return false;
                }
                cp = (cp << 6) | (c & 0x3F);
            }
            if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) {
                return false;
            }
            i += more + 1;
        }
        return true;
    }

    // A new array holding a followed by b; neither input is changed.
    function concat(a as ByteArray, b as ByteArray) as ByteArray {
        var out = new [0]b;
        out.addAll(a);
        out.addAll(b);
        return out;
    }

    // A new array with a copy of data.
    function copy(data as ByteArray) as ByteArray {
        var out = new [0]b;
        out.addAll(data);
        return out;
    }

    // Overwrite every byte with zero. Accepts null so callers can wipe fields
    // that may not have been set.
    function wipe(data as ByteArray?) as Void {
        if (data == null) {
            return;
        }
        for (var i = 0; i < data.size(); i++) {
            data[i] = 0;
        }
    }

    // True when both arrays have the same length and content. The running time
    // depends only on the length, not on where the first difference is.
    function equal(a as ByteArray, b as ByteArray) as Boolean {
        if (a.size() != b.size()) {
            return false;
        }
        var diff = 0;
        for (var i = 0; i < a.size(); i++) {
            diff = diff | (a[i] ^ b[i]);
        }
        return diff == 0;
    }

    // XOR source into target, byte by byte (same length required).
    function xorInto(target as ByteArray, source as ByteArray) as Void {
        for (var i = 0; i < target.size(); i++) {
            target[i] = target[i] ^ source[i];
        }
    }

    // An unsigned 64-bit value as 8 bytes, most significant first (the HOTP
    // counter encoding of RFC 4226).
    function uint64BigEndian(value as Long) as ByteArray {
        var out = new [8]b;
        var v = value;
        for (var i = 7; i >= 0; i--) {
            out[i] = (v & 0xFFl).toNumber();
            v = v >> 8;
        }
        return out;
    }

    // An unsigned 32-bit value as 4 bytes, most significant first.
    function uint32BigEndian(value as Number) as ByteArray {
        var out = new [4]b;
        out.encodeNumber(value, Lang.NUMBER_FORMAT_UINT32, {:offset => 0, :endianness => Lang.ENDIAN_BIG});
        return out;
    }
}