// 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; } }