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
|
// Author: cirakg & claude ai
// Summary: HOTP (RFC 4226), TOTP (RFC 6238) and Steam Guard code generation.
//
// All three share HOTP's dynamic truncation of an HMAC over the 8-byte
// big-endian counter:
// * HOTP: the counter is stored with the entry.
// * TOTP: counter = floor(unix time / period). Time is a 64-bit Long, so
// the maths keeps working after 2038 (Clock.mc widens the watch's 32-bit
// clock value) and the RFC 6238 vector for the year 2603 passes.
// * Steam: TOTP with SHA-1 and a 30-second period, written as 5 characters
// from Steam's 26-letter alphabet instead of decimal digits. Optional entry
// type, kept separate here.
//
// Security note: callers decrypt the secret just for the call and wipe it
// afterwards (README 7.8); nothing here keeps a copy.
// Verified with RFC 4226 Appendix D, RFC 6238 Appendix B and seeded vectors
// from an independent Python implementation (test/otp/OtpTest.mc).
import Toybox.Lang;
module Otp {
const MIN_DIGITS = 6;
const MAX_DIGITS = 8;
const MIN_PERIOD = 1;
const MAX_PERIOD = 300;
const STEAM_PERIOD = 30;
const STEAM_LENGTH = 5;
const STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY"; // lint: allow-literal
const POWERS_OF_TEN = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000];
// RFC 4226 dynamic truncation: a 31-bit number from HMAC(secret, counter).
function truncate(alg as Hashes.Algorithm, secret as ByteArray, counter as Long) as Number {
var mac = Hmac.compute(alg, secret, Bytes.uint64BigEndian(counter));
var offset = mac[mac.size() - 1] & 0x0F;
var value = ((mac[offset] & 0x7F) << 24) | (mac[offset + 1] << 16) | (mac[offset + 2] << 8) | mac[offset + 3];
Bytes.wipe(mac);
return value;
}
// HOTP code with `digits` digits (6 to 8), zero-padded.
function hotp(alg as Hashes.Algorithm, secret as ByteArray, counter as Long, digits as Number) as String {
if (digits < MIN_DIGITS || digits > MAX_DIGITS) {
throw new Lang.InvalidValueException("OTP: digits must be 6 to 8");
}
var code = truncate(alg, secret, counter) % (POWERS_OF_TEN[digits] as Number);
return code.format("%0" + digits + "d");
}
// The TOTP counter (time step) for a unix time; times before 1970 count as 0.
function totpCounter(unixTime as Long, period as Number) as Long {
if (period < MIN_PERIOD || period > MAX_PERIOD) {
throw new Lang.InvalidValueException("OTP: period must be 1 to 300 seconds");
}
return unixTime < 0 ? 0l : unixTime / period;
}
// TOTP code at a unix time.
function totp(alg as Hashes.Algorithm, secret as ByteArray, unixTime as Long, period as Number, digits as Number) as String {
return hotp(alg, secret, totpCounter(unixTime, period), digits);
}
// Seconds until the code changes (1 to period).
function secondsRemaining(unixTime as Long, period as Number) as Number {
if (unixTime < 0) {
return period;
}
return period - (unixTime % period).toNumber();
}
// Steam Guard code at a unix time.
function steam(secret as ByteArray, unixTime as Long) as String {
var value = truncate(Hashes.SHA1, secret, totpCounter(unixTime, STEAM_PERIOD));
var code = "";
for (var i = 0; i < STEAM_LENGTH; i++) {
var index = value % 26;
code += STEAM_ALPHABET.substring(index, index + 1);
value = value / 26;
}
return code;
}
}
|