// Author: cirakg & claude ai // Summary: One wallet entry (an OTP account or a barcode card): its fields, limits, validation and wiping. // // The fields follow README 6.1. Header fields are readable without a key so // the list draws without decrypting; the sensitive fields (algorithm, digits, // period, secret, payload, options) only exist in memory after the vault has // opened the record, and wipeSecrets() zeroes them again. // // An Entry that came from Vault.header() has secret == null and // payload == null; one from Vault.open() has them filled in. import Toybox.Lang; class Entry { // kind static const KIND_OTP = 0; static const KIND_CODE = 1; // type, for OTP entries (code entries store their symbology, M6) static const OTP_TOTP = 0; static const OTP_HOTP = 1; static const OTP_STEAM = 2; // source static const SOURCE_MANUAL = 0; static const SOURCE_IMPORT = 1; // ID alphabet: digits and capitals without 0, 1, I, L, O and U, so IDs // are easy to read off the watch and type into the Manage form. static const ID_ALPHABET = "23456789ABCDEFGHJKMNPQRSTVWXYZ"; // lint: allow-literal static const ID_LENGTH = 4; // Limits, in UTF-8 bytes. They keep one record far below the 8 KB // per-value limit of Application.Storage (README 6.2). static const MAX_NAME_BYTES = 64; static const MAX_GROUP_BYTES = 32; static const MAX_SECRET_BYTES = 128; static const MAX_PAYLOAD_BYTES = 4096; static const MAX_SYMBOLOGY = 31; // Header fields. var id as String = ""; var kind as Number = KIND_OTP; var type as Number = OTP_TOTP; var name as String = ""; var account as String = ""; var group as String = ""; var locked as Boolean = false; var source as Number = SOURCE_MANUAL; var favorite as Boolean = false; var counter as Long = 0l; var created as Long = 0l; var namesEncrypted as Boolean = false; // Sensitive fields (encrypted at rest). var algorithm as Number = Hashes.SHA1; var digits as Number = 6; var period as Number = 30; var secret as ByteArray? = null; var payload as ByteArray? = null; var options as Number = 0; function initialize() { } // True for a well-formed ID: ID_LENGTH characters from ID_ALPHABET. static function isValidId(value as String) as Boolean { if (value.length() != ID_LENGTH) { return false; } var chars = value.toCharArray(); for (var i = 0; i < chars.size(); i++) { if (ID_ALPHABET.find(chars[i].toString()) == null) { return false; } } return true; } // null when the entry can be stored, otherwise a short English reason. // Checks the ID only when one is set (the vault assigns new IDs). // Settings and import validate user input in more detail (M5, M7); this is // the last line of defence before anything is written. function validate() as String? { if (id.length() > 0 && !isValidId(id)) { return "bad id"; } if (Bytes.fromUtf8(name).size() > MAX_NAME_BYTES || Bytes.fromUtf8(account).size() > MAX_NAME_BYTES) { return "name too long"; } if (Bytes.fromUtf8(group).size() > MAX_GROUP_BYTES) { return "group too long"; } if (source != SOURCE_MANUAL && source != SOURCE_IMPORT) { return "bad source"; } if (counter < 0) { return "bad counter"; } if (kind == KIND_OTP) { return validateOtp(); } if (kind == KIND_CODE) { return validateCode(); } return "bad kind"; } private function validateOtp() as String? { if (type != OTP_TOTP && type != OTP_HOTP && type != OTP_STEAM) { return "bad otp type"; } if (!Hashes.isValid(algorithm)) { return "bad algorithm"; } if (digits < Otp.MIN_DIGITS || digits > Otp.MAX_DIGITS) { return "bad digits"; } if (period < Otp.MIN_PERIOD || period > Otp.MAX_PERIOD) { return "bad period"; } var s = secret; if (s == null || s.size() == 0 || s.size() > MAX_SECRET_BYTES) { return "bad secret"; } return null; } private function validateCode() as String? { if (type < 0 || type > MAX_SYMBOLOGY) { return "bad symbology"; } var p = payload; if (p == null || p.size() == 0 || p.size() > MAX_PAYLOAD_BYTES) { return "bad payload"; } return null; } // Zero the secret and payload in place and drop them. Call as soon as a // code has been computed or a barcode encoded (README 7.8). function wipeSecrets() as Void { Bytes.wipe(secret); Bytes.wipe(payload); secret = null; payload = null; } }