@lumjs/encode 2.1.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,7 +6,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [2.1.0]
9
+ ## [2.2.2] - 2026-01-13
10
+ ### Fixed
11
+ - A few typos and bugs in the HMAC/HOTP/TOTP libraries I added last time.
12
+ - Added some missing timestamps in this changelog.
13
+ - Fixed references in this changelog.
14
+ ### Changed
15
+ - While fixing the broken bits, I made a bunch of previously hard-coded values
16
+ into options. They'll need to be properly documented at some point.
17
+ - Changed how `hash.getAlgorithm()` works behind the scenes.
18
+ - v2.2.1 never got published for reasons, so no log or tag for it.
19
+
20
+ ## [2.2.0] - 2025-11-18
21
+ ### Added
22
+ - intToBytes() and hexToBytes() functions added to util.js
23
+ - A HMAC wrapper library, and a generic Signature class used by it.
24
+ - HOTP/TOTP generation and valdiation classes.
25
+ They are loosely based on the ones from the `notp` package, but while that
26
+ package exports some static functions and depends on the node.js `crypto`
27
+ module, this one has two JS classes and uses the SubtleCrypto API.
28
+ - A TODO file with things I want to do.
29
+
30
+ ## [2.1.0] - 2025-90-09
10
31
  ### Changed
11
32
  - The `base64` library now supports the `Uint8Array.fromBase64` static method,
12
33
  and the corresponding `toBase64()` instance method. As those are rather new
@@ -42,7 +63,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
42
63
  ### Added
43
64
  - Initial release.
44
65
 
45
- [Unreleased]: https://github.com/supernovus/lum.encode.js/compare/v2.1.0...HEAD
66
+ [Unreleased]: https://github.com/supernovus/lum.encode.js/compare/v2.2.2...HEAD
67
+ [2.2.2]: https://github.com/supernovus/lum.encode.js/compare/v2.2.0...v2.2.2
68
+ [2.2.0]: https://github.com/supernovus/lum.encode.js/compare/v2.1.0...v2.2.0
46
69
  [2.1.0]: https://github.com/supernovus/lum.encode.js/compare/v2.0.0...v2.1.0
47
70
  [2.0.0]: https://github.com/supernovus/lum.encode.js/compare/v1.2.0...v2.0.0
48
71
  [1.2.0]: https://github.com/supernovus/lum.encode.js/compare/v1.1.0...v1.2.0
package/TODO.md ADDED
@@ -0,0 +1,3 @@
1
+ # TODO
2
+
3
+ - Write tests for HMAC, HOTP, TOTP, etc.
package/lib/hash.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict';
2
+
1
3
  const {S,F,isObj,isNil} = require('@lumjs/core/types');
2
4
 
3
5
  const util = require('./util');
@@ -25,6 +27,11 @@ const ALGO_INFO =
25
27
  'SHA-512': {length: 512, block: 1024},
26
28
  }
27
29
 
30
+ for (let algo in ALGO_INFO) {
31
+ ALGO_INFO[algo].id = algo;
32
+ Object.freeze(ALGO_INFO[algo]);
33
+ }
34
+
28
35
  const DATA_ENCODERS =
29
36
  {
30
37
  base64, base91,
@@ -182,7 +189,7 @@ module.exports = class
182
189
 
183
190
  if (id in ALGO_INFO)
184
191
  {
185
- return Object.assign({id}, ALGO_INFO[id]);
192
+ return ALGO_INFO[id];
186
193
  }
187
194
 
188
195
  // Invalid algorithm id, nothing to return.
@@ -297,8 +304,8 @@ module.exports = class
297
304
  async base64(input, opts=this.defaults.base64)
298
305
  {
299
306
  const hash = await this.hash(input);
300
- const b64str = base64.fromBytes(new Uint8Array(hash));
301
- return opts.url ? base64.urlize(b64str) : b64str;
307
+ const b64str = base64.fromBytes(new Uint8Array(hash), opts);
308
+ return opts.url ? base64.urlize(b64str, opts) : b64str;
302
309
  }
303
310
 
304
311
  /**
@@ -330,7 +337,7 @@ module.exports = class
330
337
 
331
338
  if (nba)
332
339
  {
333
- hash = util.numByteArray(hash);
340
+ hash = util.numByteArray(hash, nba);
334
341
  }
335
342
 
336
343
  return base91.encode(new Uint8Array(hash));
@@ -341,7 +348,7 @@ module.exports = class
341
348
  *
342
349
  * @param {(string|object)} input - A value to add to the hash.
343
350
  *
344
- * If it is an `object` then it will be processed with the `addWith`
351
+ * If it is an `object` then it will be processed with the `addUsing`
345
352
  * handler. See the constructor for details on supported formats.
346
353
  *
347
354
  * String values are simply added _as-is_.
package/lib/hmac.js ADDED
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const Signature = require('./signature');
4
+
5
+ const CKEY = Symbol('@lumjs/encore/hmac~key');
6
+ const HMAC = 'HMAC';
7
+ const DEF_OPTS = {
8
+ algorithm: 'SHA-256',
9
+ extractable: false,
10
+ keyFormat: 'raw',
11
+ usages: ['sign']
12
+ };
13
+
14
+ /**
15
+ * The main class to perform HMAC signing.
16
+ * @exports module:@lumjs/encode/hmac
17
+ */
18
+ class HmacEncoder {
19
+ /**
20
+ * Create an encoder.
21
+ *
22
+ * @param {(string|TypedArray|ArrayBuffer)} keyValue - The secret key value.
23
+ * Will be used to generate the crypto key.
24
+ * @param {object} [options] Options
25
+ * @param {string} [options.algorithm="SHA-256"] Digest algorithm for HMAC.
26
+ */
27
+ constructor(keyValue, options) {
28
+ this.te = new TextEncoder();
29
+ this.keyBytes = ArrayBuffer.isView(keyValue)
30
+ ? keyValue
31
+ : this.te.encode(keyValue);
32
+ this.options = Object.assign({}, DEF_OPTS, options);
33
+ }
34
+
35
+ /**
36
+ * Get the crypto key for this encoder instance.
37
+ * @returns {Promise<CryptoKey>}
38
+ */
39
+ async getKey() {
40
+ if (this[CKEY]) {
41
+ return this[CKEY];
42
+ }
43
+
44
+ let hmac = { name: HMAC, hash: this.options.algorithm }
45
+ let key = await crypto.subtle.importKey(
46
+ this.options.keyFormat, // Key format
47
+ this.keyBytes, // Key data
48
+ hmac, // Algorithm
49
+ this.options.extractable, // Is key extractable
50
+ this.options.usages, // Allowed key usages
51
+ );
52
+
53
+ this[CKEY] = key;
54
+ return key;
55
+ }
56
+
57
+ /**
58
+ * Sign a message (any kind of data).
59
+ * @param {(string|TypedArray|ArrayBuffer)} message - Message to be signed.
60
+ * @returns {Promise<module:@lumjs/encode/signature>}
61
+ */
62
+ async sign(message) {
63
+ if (!ArrayBuffer.isView(message)) {
64
+ message = this.te.encode(message);
65
+ }
66
+
67
+ let key = await this.getKey();
68
+ let sig = await crypto.subtle.sign(
69
+ HMAC, // Algorithm
70
+ key, // HMAC CryptoKey
71
+ message, // Data to sign
72
+ );
73
+
74
+ return new Signature(sig);
75
+ }
76
+
77
+ }
78
+
79
+ module.exports = HmacEncoder;
package/lib/hotp.js ADDED
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const HmacEncoder = require('./hmac');
4
+ const { intToBytes } = require('./util');
5
+
6
+ const DEF_OPTS = {
7
+ algorithm: 'SHA-1',
8
+ checkSize: 7,
9
+ counter: 0,
10
+ window: 50
11
+ };
12
+
13
+ const cp = Object.assign;
14
+ const isError = v => (typeof v === 'function' && Error.isPrototypeOf(v));
15
+
16
+ function needKey(key) {
17
+ if (!key) {
18
+ throw new Error("No signing key was specified");
19
+ }
20
+ }
21
+
22
+ /**
23
+ * HMAC-based One-Time-Passwords.
24
+ * @exports module:@lumjs/encode/hotp
25
+ */
26
+ class HOTP {
27
+ constructor(options) {
28
+ this.setOptions(options);
29
+ }
30
+
31
+ setOptions(options) {
32
+ if (this.options) {
33
+ cp(this.options, options);
34
+ }
35
+ else {
36
+ this.options = cp({}, ...this.defaultOptions, options);
37
+ }
38
+ return this;
39
+ }
40
+
41
+ getOptions() {
42
+ return cp({}, this.options, ...arguments);
43
+ }
44
+
45
+ get defaultKey() {
46
+ return this.options.key;
47
+ }
48
+
49
+ get defaultOptions() {
50
+ return [DEF_OPTS];
51
+ }
52
+
53
+ async generate(key = this.defaultKey, opts) {
54
+ needKey(key);
55
+ opts = this.getOptions(opts);
56
+
57
+ let encoder = new HmacEncoder(key, opts);
58
+ let data = new Uint8Array(intToBytes(opts.counter));
59
+ let hash = await encoder.sign(data);
60
+ let hb = hash.byteArray;
61
+
62
+ let offset = hb[19] & 0xf;
63
+ let v1 =
64
+ (hb[offset] & 0x7f) << 24 |
65
+ (hb[offset + 1] & 0xff) << 16 |
66
+ (hb[offset + 2] & 0xff) << 8 |
67
+ (hb[offset + 3] & 0xff);
68
+
69
+ let v2 = (v1 % 1000000) + '';
70
+ let code = Array(opts.checkSize - v2.length).join('0') + v2;
71
+
72
+ let res = {
73
+ opts,
74
+ data,
75
+ hash,
76
+ hashBytes: hb,
77
+ offset,
78
+ v1,
79
+ v2,
80
+ code,
81
+ toString,
82
+ }
83
+
84
+ if (opts.debug) console.debug(res.code, res);
85
+
86
+ return res;
87
+ }
88
+
89
+ async verify(token, key = this.defaultKey, opts) {
90
+ needKey(key);
91
+ opts = this.getOptions(opts);
92
+
93
+ let win = opts.window;
94
+ let cnt = opts.counter;
95
+ let info = { ok: false };
96
+
97
+ if (opts.debug) info.stack = [];
98
+
99
+ for (let i = cnt - win; i <= cnt + win; ++i) {
100
+ opts.counter = i;
101
+ let res = this.generate(key, opts);
102
+ if (opts.debug) info.stack.push(res);
103
+ if (res.code === token) {
104
+ return cp(info, { ok: true, delta: i - cnt });
105
+ }
106
+ }
107
+
108
+ if (opts.throw) {
109
+ let EClass = isError(opts.throw) ? opts.throw : Error;
110
+ throw new EClass("OTP verification failure");
111
+ }
112
+
113
+ return info;
114
+ }
115
+ }
116
+
117
+ module.exports = HOTP;
package/lib/index.js CHANGED
@@ -9,8 +9,15 @@ const E = def.e;
9
9
 
10
10
  const util = require('./util');
11
11
 
12
+ /**
13
+ * @alias module:@lumjs/encode.util
14
+ * @see {@link module:@lumjs/encode/util}
15
+ */
16
+ def(exports, 'util', {value: util}, E);
17
+
12
18
  /**
13
19
  * @alias module:@lumjs/encode.ord
20
+ * @deprecated this alias to `util.ord` will be removed in 3.x.
14
21
  * @see {@link module:@lumjs/encode/util.ord}
15
22
  */
16
23
  def(exports, 'ord', util.ord, E);
@@ -18,6 +25,7 @@ def(exports, 'ord', util.ord, E);
18
25
  /**
19
26
  * @name module:@lumjs/encode.numByteArray
20
27
  * @function
28
+ * @deprecated this alias to `util.numByteArray` will be removed in 3.x.
21
29
  * @see {@link module:@lumjs/encode/util.numByteArray}
22
30
  */
23
31
  def(exports, 'numByteArray', util.numByteArray, E);
@@ -39,3 +47,27 @@ lazy(exports, 'Base91', () => require('./base91'), E);
39
47
  * @see {@link module:@lumjs/encode/hash}
40
48
  */
41
49
  lazy(exports, 'Hash', () => require('./hash'), E);
50
+
51
+ /**
52
+ * @name module:@lumjs/encode.HMAC
53
+ * @see {@link module:@lumjs/encode/hmac}
54
+ */
55
+ lazy(exports, 'HMAC', () => require('./hmac'), E);
56
+
57
+ /**
58
+ * @name module:@lumjs/encode.HOTP
59
+ * @see {@link module:@lumjs/encode/hotp}
60
+ */
61
+ lazy(exports, 'HOTP', () => require('./hotp'), E);
62
+
63
+ /**
64
+ * @name module:@lumjs/encode.TOTP
65
+ * @see {@link module:@lumjs/encode/totp}
66
+ */
67
+ lazy(exports, 'TOTP', () => require('./totp'), E);
68
+
69
+ /**
70
+ * @name module:@lumjs/encode.Signature
71
+ * @see {@link module:@lumjs/encode/signature}
72
+ */
73
+ lazy(exports, 'Signature', () => require('./signature'), E);
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A small wrapepr class representing a crypto signature.
5
+ * @alias module:@lumjs/encode/signature
6
+ */
7
+ class Signature {
8
+ /**
9
+ * Build a Signature instance.
10
+ * @param {ArrayBuffer} buffer - The signature data.
11
+ */
12
+ constructor(buffer) {
13
+ this.buffer = buffer;
14
+ }
15
+
16
+ /**
17
+ * Get the signature as a Uint8Array.
18
+ */
19
+ get uint8Array() {
20
+ return new Uint8Array(this.buffer);
21
+ }
22
+
23
+ /**
24
+ * Get the signature as an array of bytes.
25
+ */
26
+ get byteArray() {
27
+ return Array.from(this.uint8Array);
28
+ }
29
+
30
+ /**
31
+ * Get the signature as a Hex string.
32
+ */
33
+ get hex() {
34
+ return this.byteArray.map(b => b.toString(16).padStart(2, '0')).join('');
35
+ }
36
+ }
package/lib/totp.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const HOTP = require('./hotp');
4
+ const DEF_OPTS = {step: 30};
5
+
6
+ /**
7
+ * Time-based One-Time-Passwords.
8
+ * @exports module:@lumjs/encode/totp
9
+ */
10
+ class TOTP extends HOTP {
11
+ get defaultOptions() {
12
+ return [...super.defaultOptions, DEF_OPTS];
13
+ }
14
+
15
+ getOptions() {
16
+ let opts = super.getOptions(...arguments);
17
+ if (!opts.time) opts.time = Date.now();
18
+ opts.counter = Math.floor((opts.time / 1000) / opts.step);
19
+ return opts;
20
+ }
21
+ }
22
+
23
+ module.exports = TOTP;
package/lib/util.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * Low-level encoding utilities
3
3
  * @module @lumjs/encode/util
4
4
  */
5
+ 'use strict';
6
+
5
7
  const {N} = require('@lumjs/core/types');
6
8
 
7
9
  /**
@@ -189,3 +191,42 @@ exports.wordArrayToUint8Array = function(wordArray)
189
191
  }
190
192
  return result;
191
193
  }
194
+
195
+ /**
196
+ * Convert an integer to a byte array.
197
+ *
198
+ * This is a much simpler algorithm than numByteArray,
199
+ * and was borrowed from the `notp` package.
200
+ *
201
+ * @param {Integer} num
202
+ * @return {Array} bytes
203
+ */
204
+ exports.intToBytes = function(num)
205
+ {
206
+ let bytes = [];
207
+
208
+ for(let i=7 ; i>=0 ; --i)
209
+ {
210
+ bytes[i] = num & (255);
211
+ num = num >> 8;
212
+ }
213
+
214
+ return bytes;
215
+ }
216
+
217
+ /**
218
+ * Convert a hex value to a byte array.
219
+ *
220
+ * Also taken from the `notp` package.
221
+ *
222
+ * @param {String} hex string of hex to convert to a byte array
223
+ * @return {Array} bytes
224
+ */
225
+ exports.hexToBytes = function(hex) {
226
+ var bytes = [];
227
+ for(var c = 0, C = hex.length; c < C; c += 2) {
228
+ bytes.push(parseInt(hex.substr(c, 2), 16));
229
+ }
230
+ return bytes;
231
+ }
232
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/encode",
3
- "version": "2.1.0",
3
+ "version": "2.2.2",
4
4
  "main": "lib/index.js",
5
5
  "exports":
6
6
  {
@@ -8,6 +8,10 @@
8
8
  "./base64": "./lib/base64.js",
9
9
  "./base91": "./lib/base91.js",
10
10
  "./hash": "./lib/hash.js",
11
+ "./hmac": "./lib/hmac.js",
12
+ "./hotp": "./lib/hotp.js",
13
+ "./signature": "./lib/signature.js",
14
+ "./totp": "./lib/totp.js",
11
15
  "./util": "./lib/util.js",
12
16
  "./package.json": "./package.json"
13
17
  },
@@ -19,7 +23,7 @@
19
23
  },
20
24
  "dependencies":
21
25
  {
22
- "@lumjs/core": "^1.26.0"
26
+ "@lumjs/core": "^1.28.0"
23
27
  },
24
28
  "devDependencies":
25
29
  {