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
|
// Author: cirakg & claude ai
// Summary: Base32 (RFC 4648) decoding of OTP secrets with precise error positions, and encoding.
//
// Secrets arrive typed by hand or pasted, so decode() is lenient where it is
// safe: any letter case, spaces, tabs and hyphens are ignored, and "="
// padding is optional. Everything else fails with the 1-based position of
// the offending character in the original text, for the status messages in
// README 10.4. Leftover bits after the last full byte are ignored (many
// services emit secrets whose length isn't a multiple of 8 characters).
//
// Verified with RFC 4648 section 10 and seeded round trips
// (test/otp/Base32Test.mc).
import Toybox.Lang;
module Base32 {
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; // lint: allow-literal
const PAD = 61; // '='
// Why a decode failed: a message, and the 1-based character position (0
// when the problem is the length rather than one character).
class DecodeError {
var position as Number;
var message as String;
function initialize(pos as Number, msg as String) {
position = pos;
message = msg;
}
}
// Value 0-31 of one Base32 character code, -1 if it isn't one.
function valueOf(code as Number) as Number {
if (code >= 65 && code <= 90) { // A-Z
return code - 65;
}
if (code >= 97 && code <= 122) { // a-z
return code - 97;
}
if (code >= 50 && code <= 55) { // 2-7
return code - 24;
}
return -1;
}
// True for the separators people type into secrets: space, tab, hyphen.
function isIgnorable(code as Number) as Boolean {
return code == 32 || code == 9 || code == 45;
}
// Bytes of a Base32 text, or a DecodeError.
function decode(text as String) as ByteArray or DecodeError {
var chars = text.toCharArray();
var out = new [0]b;
var buffer = 0;
var bits = 0;
var symbols = 0;
var pads = 0;
for (var i = 0; i < chars.size(); i++) {
var code = chars[i].toNumber();
if (isIgnorable(code)) {
continue;
}
if (code == PAD) {
pads++;
continue;
}
var value = valueOf(code);
if (value < 0) {
return new DecodeError(i + 1, "'" + chars[i].toString() + "' is not a Base32 character");
}
if (pads > 0) {
return new DecodeError(i + 1, "characters after '=' padding");
}
buffer = ((buffer << 5) | value) & 0xFFF;
bits += 5;
symbols++;
if (bits >= 8) {
bits -= 8;
out.add((buffer >> bits) & 0xFF);
}
}
var remainder = symbols % 8;
if (remainder == 1 || remainder == 3 || remainder == 6) {
return new DecodeError(0, "incomplete Base32 group: " + symbols + " characters can't be a whole number of bytes");
}
if (pads > 0 && (symbols + pads) % 8 != 0) {
return new DecodeError(0, "wrong amount of '=' padding");
}
return out;
}
// RFC 4648 Base32 text with "=" padding.
function encode(data as ByteArray) as String {
var result = "";
var buffer = 0;
var bits = 0;
for (var i = 0; i < data.size(); i++) {
buffer = ((buffer << 8) | data[i]) & 0xFFFF;
bits += 8;
while (bits >= 5) {
bits -= 5;
result += ALPHABET.substring((buffer >> bits) & 31, ((buffer >> bits) & 31) + 1);
}
}
if (bits > 0) {
var index = (buffer << (5 - bits)) & 31;
result += ALPHABET.substring(index, index + 1);
}
while (result.length() % 8 != 0) {
result += "=";
}
return result;
}
}
|