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