otpauth.node.mjs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. //! otpauth 9.3.1 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
  2. /// <reference types="./otpauth.d.ts" />
  3. // @ts-nocheck
  4. import * as crypto from 'node:crypto';
  5. /**
  6. * Converts an integer to an Uint8Array.
  7. * @param {number} num Integer.
  8. * @returns {Uint8Array} Uint8Array.
  9. */ const uintDecode = (num)=>{
  10. const buf = new ArrayBuffer(8);
  11. const arr = new Uint8Array(buf);
  12. let acc = num;
  13. for(let i = 7; i >= 0; i--){
  14. if (acc === 0) break;
  15. arr[i] = acc & 255;
  16. acc -= arr[i];
  17. acc /= 256;
  18. }
  19. return arr;
  20. };
  21. /**
  22. * "globalThis" ponyfill.
  23. * @see [A horrifying globalThis polyfill in universal JavaScript](https://mathiasbynens.be/notes/globalthis)
  24. * @type {Object.<string, *>}
  25. */ const globalScope = (()=>{
  26. if (typeof globalThis === "object") return globalThis;
  27. else {
  28. Object.defineProperty(Object.prototype, "__GLOBALTHIS__", {
  29. get () {
  30. return this;
  31. },
  32. configurable: true
  33. });
  34. try {
  35. // @ts-ignore
  36. // eslint-disable-next-line no-undef
  37. if (typeof __GLOBALTHIS__ !== "undefined") return __GLOBALTHIS__;
  38. } finally{
  39. // @ts-ignore
  40. delete Object.prototype.__GLOBALTHIS__;
  41. }
  42. }
  43. // Still unable to determine "globalThis", fall back to a naive method.
  44. if (typeof self !== "undefined") return self;
  45. else if (typeof window !== "undefined") return window;
  46. else if (typeof global !== "undefined") return global;
  47. return undefined;
  48. })();
  49. /**
  50. * Calculates an HMAC digest.
  51. * In Node.js, the command "openssl list -digest-algorithms" displays the available digest algorithms.
  52. * @param {string} algorithm Algorithm.
  53. * @param {Uint8Array} key Key.
  54. * @param {Uint8Array} message Message.
  55. * @returns {Uint8Array} Digest.
  56. */ const hmacDigest = (algorithm, key, message)=>{
  57. if (crypto?.createHmac) {
  58. const hmac = crypto.createHmac(algorithm, globalScope.Buffer.from(key));
  59. hmac.update(globalScope.Buffer.from(message));
  60. return hmac.digest();
  61. } else {
  62. throw new Error("Missing HMAC function");
  63. }
  64. };
  65. /**
  66. * RFC 4648 base32 alphabet without pad.
  67. * @type {string}
  68. */ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  69. /**
  70. * Converts a base32 string to an Uint8Array (RFC 4648).
  71. * @see [LinusU/base32-decode](https://github.com/LinusU/base32-decode)
  72. * @param {string} str Base32 string.
  73. * @returns {Uint8Array} Uint8Array.
  74. */ const base32Decode = (str)=>{
  75. // Canonicalize to all upper case and remove padding if it exists.
  76. let end = str.length;
  77. while(str[end - 1] === "=")--end;
  78. const cstr = (end < str.length ? str.substring(0, end) : str).toUpperCase();
  79. const buf = new ArrayBuffer(cstr.length * 5 / 8 | 0);
  80. const arr = new Uint8Array(buf);
  81. let bits = 0;
  82. let value = 0;
  83. let index = 0;
  84. for(let i = 0; i < cstr.length; i++){
  85. const idx = ALPHABET.indexOf(cstr[i]);
  86. if (idx === -1) throw new TypeError(`Invalid character found: ${cstr[i]}`);
  87. value = value << 5 | idx;
  88. bits += 5;
  89. if (bits >= 8) {
  90. bits -= 8;
  91. arr[index++] = value >>> bits;
  92. }
  93. }
  94. return arr;
  95. };
  96. /**
  97. * Converts an Uint8Array to a base32 string (RFC 4648).
  98. * @see [LinusU/base32-encode](https://github.com/LinusU/base32-encode)
  99. * @param {Uint8Array} arr Uint8Array.
  100. * @returns {string} Base32 string.
  101. */ const base32Encode = (arr)=>{
  102. let bits = 0;
  103. let value = 0;
  104. let str = "";
  105. for(let i = 0; i < arr.length; i++){
  106. value = value << 8 | arr[i];
  107. bits += 8;
  108. while(bits >= 5){
  109. str += ALPHABET[value >>> bits - 5 & 31];
  110. bits -= 5;
  111. }
  112. }
  113. if (bits > 0) {
  114. str += ALPHABET[value << 5 - bits & 31];
  115. }
  116. return str;
  117. };
  118. /**
  119. * Converts a hexadecimal string to an Uint8Array.
  120. * @param {string} str Hexadecimal string.
  121. * @returns {Uint8Array} Uint8Array.
  122. */ const hexDecode = (str)=>{
  123. const buf = new ArrayBuffer(str.length / 2);
  124. const arr = new Uint8Array(buf);
  125. for(let i = 0; i < str.length; i += 2){
  126. arr[i / 2] = parseInt(str.substring(i, i + 2), 16);
  127. }
  128. return arr;
  129. };
  130. /**
  131. * Converts an Uint8Array to a hexadecimal string.
  132. * @param {Uint8Array} arr Uint8Array.
  133. * @returns {string} Hexadecimal string.
  134. */ const hexEncode = (arr)=>{
  135. let str = "";
  136. for(let i = 0; i < arr.length; i++){
  137. const hex = arr[i].toString(16);
  138. if (hex.length === 1) str += "0";
  139. str += hex;
  140. }
  141. return str.toUpperCase();
  142. };
  143. /**
  144. * Converts a Latin-1 string to an Uint8Array.
  145. * @param {string} str Latin-1 string.
  146. * @returns {Uint8Array} Uint8Array.
  147. */ const latin1Decode = (str)=>{
  148. const buf = new ArrayBuffer(str.length);
  149. const arr = new Uint8Array(buf);
  150. for(let i = 0; i < str.length; i++){
  151. arr[i] = str.charCodeAt(i) & 0xff;
  152. }
  153. return arr;
  154. };
  155. /**
  156. * Converts an Uint8Array to a Latin-1 string.
  157. * @param {Uint8Array} arr Uint8Array.
  158. * @returns {string} Latin-1 string.
  159. */ const latin1Encode = (arr)=>{
  160. let str = "";
  161. for(let i = 0; i < arr.length; i++){
  162. str += String.fromCharCode(arr[i]);
  163. }
  164. return str;
  165. };
  166. /**
  167. * TextEncoder instance.
  168. * @type {TextEncoder|null}
  169. */ const ENCODER = globalScope.TextEncoder ? new globalScope.TextEncoder() : null;
  170. /**
  171. * TextDecoder instance.
  172. * @type {TextDecoder|null}
  173. */ const DECODER = globalScope.TextDecoder ? new globalScope.TextDecoder() : null;
  174. /**
  175. * Converts an UTF-8 string to an Uint8Array.
  176. * @param {string} str String.
  177. * @returns {Uint8Array} Uint8Array.
  178. */ const utf8Decode = (str)=>{
  179. if (!ENCODER) {
  180. throw new Error("Encoding API not available");
  181. }
  182. return ENCODER.encode(str);
  183. };
  184. /**
  185. * Converts an Uint8Array to an UTF-8 string.
  186. * @param {Uint8Array} arr Uint8Array.
  187. * @returns {string} String.
  188. */ const utf8Encode = (arr)=>{
  189. if (!DECODER) {
  190. throw new Error("Encoding API not available");
  191. }
  192. return DECODER.decode(arr);
  193. };
  194. /**
  195. * Returns random bytes.
  196. * @param {number} size Size.
  197. * @returns {Uint8Array} Random bytes.
  198. */ const randomBytes = (size)=>{
  199. if (crypto?.randomBytes) {
  200. return crypto.randomBytes(size);
  201. } else {
  202. if (!globalScope.crypto?.getRandomValues) {
  203. throw new Error("Cryptography API not available");
  204. }
  205. return globalScope.crypto.getRandomValues(new Uint8Array(size));
  206. }
  207. };
  208. /**
  209. * OTP secret key.
  210. */ class Secret {
  211. /**
  212. * Converts a Latin-1 string to a Secret object.
  213. * @param {string} str Latin-1 string.
  214. * @returns {Secret} Secret object.
  215. */ static fromLatin1(str) {
  216. return new Secret({
  217. buffer: latin1Decode(str).buffer
  218. });
  219. }
  220. /**
  221. * Converts an UTF-8 string to a Secret object.
  222. * @param {string} str UTF-8 string.
  223. * @returns {Secret} Secret object.
  224. */ static fromUTF8(str) {
  225. return new Secret({
  226. buffer: utf8Decode(str).buffer
  227. });
  228. }
  229. /**
  230. * Converts a base32 string to a Secret object.
  231. * @param {string} str Base32 string.
  232. * @returns {Secret} Secret object.
  233. */ static fromBase32(str) {
  234. return new Secret({
  235. buffer: base32Decode(str).buffer
  236. });
  237. }
  238. /**
  239. * Converts a hexadecimal string to a Secret object.
  240. * @param {string} str Hexadecimal string.
  241. * @returns {Secret} Secret object.
  242. */ static fromHex(str) {
  243. return new Secret({
  244. buffer: hexDecode(str).buffer
  245. });
  246. }
  247. /**
  248. * Secret key buffer.
  249. * @deprecated For backward compatibility, the "bytes" property should be used instead.
  250. * @type {ArrayBufferLike}
  251. */ get buffer() {
  252. return this.bytes.buffer;
  253. }
  254. /**
  255. * Latin-1 string representation of secret key.
  256. * @type {string}
  257. */ get latin1() {
  258. Object.defineProperty(this, "latin1", {
  259. enumerable: true,
  260. writable: false,
  261. configurable: false,
  262. value: latin1Encode(this.bytes)
  263. });
  264. return this.latin1;
  265. }
  266. /**
  267. * UTF-8 string representation of secret key.
  268. * @type {string}
  269. */ get utf8() {
  270. Object.defineProperty(this, "utf8", {
  271. enumerable: true,
  272. writable: false,
  273. configurable: false,
  274. value: utf8Encode(this.bytes)
  275. });
  276. return this.utf8;
  277. }
  278. /**
  279. * Base32 string representation of secret key.
  280. * @type {string}
  281. */ get base32() {
  282. Object.defineProperty(this, "base32", {
  283. enumerable: true,
  284. writable: false,
  285. configurable: false,
  286. value: base32Encode(this.bytes)
  287. });
  288. return this.base32;
  289. }
  290. /**
  291. * Hexadecimal string representation of secret key.
  292. * @type {string}
  293. */ get hex() {
  294. Object.defineProperty(this, "hex", {
  295. enumerable: true,
  296. writable: false,
  297. configurable: false,
  298. value: hexEncode(this.bytes)
  299. });
  300. return this.hex;
  301. }
  302. /**
  303. * Creates a secret key object.
  304. * @param {Object} [config] Configuration options.
  305. * @param {ArrayBufferLike} [config.buffer] Secret key buffer.
  306. * @param {number} [config.size=20] Number of random bytes to generate, ignored if 'buffer' is provided.
  307. */ constructor({ buffer, size = 20 } = {}){
  308. /**
  309. * Secret key.
  310. * @type {Uint8Array}
  311. * @readonly
  312. */ this.bytes = typeof buffer === "undefined" ? randomBytes(size) : new Uint8Array(buffer);
  313. // Prevent the "bytes" property from being modified.
  314. Object.defineProperty(this, "bytes", {
  315. enumerable: true,
  316. writable: false,
  317. configurable: false,
  318. value: this.bytes
  319. });
  320. }
  321. }
  322. /**
  323. * Returns true if a is equal to b, without leaking timing information that would allow an attacker to guess one of the values.
  324. * @param {string} a String a.
  325. * @param {string} b String b.
  326. * @returns {boolean} Equality result.
  327. */ const timingSafeEqual = (a, b)=>{
  328. if (crypto?.timingSafeEqual) {
  329. return crypto.timingSafeEqual(globalScope.Buffer.from(a), globalScope.Buffer.from(b));
  330. } else {
  331. if (a.length !== b.length) {
  332. throw new TypeError("Input strings must have the same length");
  333. }
  334. let i = -1;
  335. let out = 0;
  336. while(++i < a.length){
  337. out |= a.charCodeAt(i) ^ b.charCodeAt(i);
  338. }
  339. return out === 0;
  340. }
  341. };
  342. /**
  343. * HOTP: An HMAC-based One-time Password Algorithm.
  344. * @see [RFC 4226](https://tools.ietf.org/html/rfc4226)
  345. */ class HOTP {
  346. /**
  347. * Default configuration.
  348. * @type {{
  349. * issuer: string,
  350. * label: string,
  351. * issuerInLabel: boolean,
  352. * algorithm: string,
  353. * digits: number,
  354. * counter: number
  355. * window: number
  356. * }}
  357. */ static get defaults() {
  358. return {
  359. issuer: "",
  360. label: "OTPAuth",
  361. issuerInLabel: true,
  362. algorithm: "SHA1",
  363. digits: 6,
  364. counter: 0,
  365. window: 1
  366. };
  367. }
  368. /**
  369. * Generates an HOTP token.
  370. * @param {Object} config Configuration options.
  371. * @param {Secret} config.secret Secret key.
  372. * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
  373. * @param {number} [config.digits=6] Token length.
  374. * @param {number} [config.counter=0] Counter value.
  375. * @returns {string} Token.
  376. */ static generate({ secret, algorithm = HOTP.defaults.algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter }) {
  377. const digest = hmacDigest(algorithm, secret.bytes, uintDecode(counter));
  378. const offset = digest[digest.byteLength - 1] & 15;
  379. const otp = ((digest[offset] & 127) << 24 | (digest[offset + 1] & 255) << 16 | (digest[offset + 2] & 255) << 8 | digest[offset + 3] & 255) % 10 ** digits;
  380. return otp.toString().padStart(digits, "0");
  381. }
  382. /**
  383. * Generates an HOTP token.
  384. * @param {Object} [config] Configuration options.
  385. * @param {number} [config.counter=this.counter++] Counter value.
  386. * @returns {string} Token.
  387. */ generate({ counter = this.counter++ } = {}) {
  388. return HOTP.generate({
  389. secret: this.secret,
  390. algorithm: this.algorithm,
  391. digits: this.digits,
  392. counter
  393. });
  394. }
  395. /**
  396. * Validates an HOTP token.
  397. * @param {Object} config Configuration options.
  398. * @param {string} config.token Token value.
  399. * @param {Secret} config.secret Secret key.
  400. * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
  401. * @param {number} config.digits Token length.
  402. * @param {number} [config.counter=0] Counter value.
  403. * @param {number} [config.window=1] Window of counter values to test.
  404. * @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
  405. */ static validate({ token, secret, algorithm, digits, counter = HOTP.defaults.counter, window = HOTP.defaults.window }) {
  406. // Return early if the token length does not match the digit number.
  407. if (token.length !== digits) return null;
  408. let delta = null;
  409. const check = (/** @type {number} */ i)=>{
  410. const generatedToken = HOTP.generate({
  411. secret,
  412. algorithm,
  413. digits,
  414. counter: i
  415. });
  416. if (timingSafeEqual(token, generatedToken)) {
  417. delta = i - counter;
  418. }
  419. };
  420. check(counter);
  421. for(let i = 1; i <= window && delta === null; ++i){
  422. check(counter - i);
  423. if (delta !== null) break;
  424. check(counter + i);
  425. if (delta !== null) break;
  426. }
  427. return delta;
  428. }
  429. /**
  430. * Validates an HOTP token.
  431. * @param {Object} config Configuration options.
  432. * @param {string} config.token Token value.
  433. * @param {number} [config.counter=this.counter] Counter value.
  434. * @param {number} [config.window=1] Window of counter values to test.
  435. * @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
  436. */ validate({ token, counter = this.counter, window }) {
  437. return HOTP.validate({
  438. token,
  439. secret: this.secret,
  440. algorithm: this.algorithm,
  441. digits: this.digits,
  442. counter,
  443. window
  444. });
  445. }
  446. /**
  447. * Returns a Google Authenticator key URI.
  448. * @returns {string} URI.
  449. */ toString() {
  450. const e = encodeURIComponent;
  451. return "otpauth://hotp/" + `${this.issuer.length > 0 ? this.issuerInLabel ? `${e(this.issuer)}:${e(this.label)}?issuer=${e(this.issuer)}&` : `${e(this.label)}?issuer=${e(this.issuer)}&` : `${e(this.label)}?`}` + `secret=${e(this.secret.base32)}&` + `algorithm=${e(this.algorithm)}&` + `digits=${e(this.digits)}&` + `counter=${e(this.counter)}`;
  452. }
  453. /**
  454. * Creates an HOTP object.
  455. * @param {Object} [config] Configuration options.
  456. * @param {string} [config.issuer=''] Account provider.
  457. * @param {string} [config.label='OTPAuth'] Account label.
  458. * @param {boolean} [config.issuerInLabel=true] Include issuer prefix in label.
  459. * @param {Secret|string} [config.secret=Secret] Secret key.
  460. * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
  461. * @param {number} [config.digits=6] Token length.
  462. * @param {number} [config.counter=0] Initial counter value.
  463. */ constructor({ issuer = HOTP.defaults.issuer, label = HOTP.defaults.label, issuerInLabel = HOTP.defaults.issuerInLabel, secret = new Secret(), algorithm = HOTP.defaults.algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter } = {}){
  464. /**
  465. * Account provider.
  466. * @type {string}
  467. */ this.issuer = issuer;
  468. /**
  469. * Account label.
  470. * @type {string}
  471. */ this.label = label;
  472. /**
  473. * Include issuer prefix in label.
  474. * @type {boolean}
  475. */ this.issuerInLabel = issuerInLabel;
  476. /**
  477. * Secret key.
  478. * @type {Secret}
  479. */ this.secret = typeof secret === "string" ? Secret.fromBase32(secret) : secret;
  480. /**
  481. * HMAC hashing algorithm.
  482. * @type {string}
  483. */ this.algorithm = algorithm.toUpperCase();
  484. /**
  485. * Token length.
  486. * @type {number}
  487. */ this.digits = digits;
  488. /**
  489. * Initial counter value.
  490. * @type {number}
  491. */ this.counter = counter;
  492. }
  493. }
  494. /**
  495. * TOTP: Time-Based One-Time Password Algorithm.
  496. * @see [RFC 6238](https://tools.ietf.org/html/rfc6238)
  497. */ class TOTP {
  498. /**
  499. * Default configuration.
  500. * @type {{
  501. * issuer: string,
  502. * label: string,
  503. * issuerInLabel: boolean,
  504. * algorithm: string,
  505. * digits: number,
  506. * period: number
  507. * window: number
  508. * }}
  509. */ static get defaults() {
  510. return {
  511. issuer: "",
  512. label: "OTPAuth",
  513. issuerInLabel: true,
  514. algorithm: "SHA1",
  515. digits: 6,
  516. period: 30,
  517. window: 1
  518. };
  519. }
  520. /**
  521. * Generates a TOTP token.
  522. * @param {Object} config Configuration options.
  523. * @param {Secret} config.secret Secret key.
  524. * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
  525. * @param {number} [config.digits=6] Token length.
  526. * @param {number} [config.period=30] Token time-step duration.
  527. * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
  528. * @returns {string} Token.
  529. */ static generate({ secret, algorithm, digits, period = TOTP.defaults.period, timestamp = Date.now() }) {
  530. return HOTP.generate({
  531. secret,
  532. algorithm,
  533. digits,
  534. counter: Math.floor(timestamp / 1000 / period)
  535. });
  536. }
  537. /**
  538. * Generates a TOTP token.
  539. * @param {Object} [config] Configuration options.
  540. * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
  541. * @returns {string} Token.
  542. */ generate({ timestamp = Date.now() } = {}) {
  543. return TOTP.generate({
  544. secret: this.secret,
  545. algorithm: this.algorithm,
  546. digits: this.digits,
  547. period: this.period,
  548. timestamp
  549. });
  550. }
  551. /**
  552. * Validates a TOTP token.
  553. * @param {Object} config Configuration options.
  554. * @param {string} config.token Token value.
  555. * @param {Secret} config.secret Secret key.
  556. * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
  557. * @param {number} config.digits Token length.
  558. * @param {number} [config.period=30] Token time-step duration.
  559. * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
  560. * @param {number} [config.window=1] Window of counter values to test.
  561. * @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
  562. */ static validate({ token, secret, algorithm, digits, period = TOTP.defaults.period, timestamp = Date.now(), window }) {
  563. return HOTP.validate({
  564. token,
  565. secret,
  566. algorithm,
  567. digits,
  568. counter: Math.floor(timestamp / 1000 / period),
  569. window
  570. });
  571. }
  572. /**
  573. * Validates a TOTP token.
  574. * @param {Object} config Configuration options.
  575. * @param {string} config.token Token value.
  576. * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
  577. * @param {number} [config.window=1] Window of counter values to test.
  578. * @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
  579. */ validate({ token, timestamp, window }) {
  580. return TOTP.validate({
  581. token,
  582. secret: this.secret,
  583. algorithm: this.algorithm,
  584. digits: this.digits,
  585. period: this.period,
  586. timestamp,
  587. window
  588. });
  589. }
  590. /**
  591. * Returns a Google Authenticator key URI.
  592. * @returns {string} URI.
  593. */ toString() {
  594. const e = encodeURIComponent;
  595. return "otpauth://totp/" + `${this.issuer.length > 0 ? this.issuerInLabel ? `${e(this.issuer)}:${e(this.label)}?issuer=${e(this.issuer)}&` : `${e(this.label)}?issuer=${e(this.issuer)}&` : `${e(this.label)}?`}` + `secret=${e(this.secret.base32)}&` + `algorithm=${e(this.algorithm)}&` + `digits=${e(this.digits)}&` + `period=${e(this.period)}`;
  596. }
  597. /**
  598. * Creates a TOTP object.
  599. * @param {Object} [config] Configuration options.
  600. * @param {string} [config.issuer=''] Account provider.
  601. * @param {string} [config.label='OTPAuth'] Account label.
  602. * @param {boolean} [config.issuerInLabel=true] Include issuer prefix in label.
  603. * @param {Secret|string} [config.secret=Secret] Secret key.
  604. * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
  605. * @param {number} [config.digits=6] Token length.
  606. * @param {number} [config.period=30] Token time-step duration.
  607. */ constructor({ issuer = TOTP.defaults.issuer, label = TOTP.defaults.label, issuerInLabel = TOTP.defaults.issuerInLabel, secret = new Secret(), algorithm = TOTP.defaults.algorithm, digits = TOTP.defaults.digits, period = TOTP.defaults.period } = {}){
  608. /**
  609. * Account provider.
  610. * @type {string}
  611. */ this.issuer = issuer;
  612. /**
  613. * Account label.
  614. * @type {string}
  615. */ this.label = label;
  616. /**
  617. * Include issuer prefix in label.
  618. * @type {boolean}
  619. */ this.issuerInLabel = issuerInLabel;
  620. /**
  621. * Secret key.
  622. * @type {Secret}
  623. */ this.secret = typeof secret === "string" ? Secret.fromBase32(secret) : secret;
  624. /**
  625. * HMAC hashing algorithm.
  626. * @type {string}
  627. */ this.algorithm = algorithm.toUpperCase();
  628. /**
  629. * Token length.
  630. * @type {number}
  631. */ this.digits = digits;
  632. /**
  633. * Token time-step duration.
  634. * @type {number}
  635. */ this.period = period;
  636. }
  637. }
  638. /**
  639. * Key URI regex (otpauth://TYPE/[ISSUER:]LABEL?PARAMETERS).
  640. * @type {RegExp}
  641. */ const OTPURI_REGEX = /^otpauth:\/\/([ht]otp)\/(.+)\?([A-Z0-9.~_-]+=[^?&]*(?:&[A-Z0-9.~_-]+=[^?&]*)*)$/i;
  642. /**
  643. * RFC 4648 base32 alphabet with pad.
  644. * @type {RegExp}
  645. */ const SECRET_REGEX = /^[2-7A-Z]+=*$/i;
  646. /**
  647. * Regex for supported algorithms.
  648. * @type {RegExp}
  649. */ const ALGORITHM_REGEX = /^SHA(?:1|224|256|384|512|3-224|3-256|3-384|3-512)$/i;
  650. /**
  651. * Integer regex.
  652. * @type {RegExp}
  653. */ const INTEGER_REGEX = /^[+-]?\d+$/;
  654. /**
  655. * Positive integer regex.
  656. * @type {RegExp}
  657. */ const POSITIVE_INTEGER_REGEX = /^\+?[1-9]\d*$/;
  658. /**
  659. * HOTP/TOTP object/string conversion.
  660. * @see [Key URI Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
  661. */ class URI {
  662. /**
  663. * Parses a Google Authenticator key URI and returns an HOTP/TOTP object.
  664. * @param {string} uri Google Authenticator Key URI.
  665. * @returns {HOTP|TOTP} HOTP/TOTP object.
  666. */ static parse(uri) {
  667. let uriGroups;
  668. try {
  669. uriGroups = uri.match(OTPURI_REGEX);
  670. // eslint-disable-next-line no-unused-vars
  671. } catch (_) {
  672. /* Handled below */ }
  673. if (!Array.isArray(uriGroups)) {
  674. throw new URIError("Invalid URI format");
  675. }
  676. // Extract URI groups.
  677. const uriType = uriGroups[1].toLowerCase();
  678. const uriLabel = uriGroups[2].split(/(?::|%3A) *(.+)/i, 2).map(decodeURIComponent);
  679. /** @type {Object.<string, string>} */ const uriParams = uriGroups[3].split("&").reduce((acc, cur)=>{
  680. const pairArr = cur.split(/=(.*)/, 2).map(decodeURIComponent);
  681. const pairKey = pairArr[0].toLowerCase();
  682. const pairVal = pairArr[1];
  683. /** @type {Object.<string, string>} */ const pairAcc = acc;
  684. pairAcc[pairKey] = pairVal;
  685. return pairAcc;
  686. }, {});
  687. // 'OTP' will be instantiated with 'config' argument.
  688. let OTP;
  689. const config = {};
  690. if (uriType === "hotp") {
  691. OTP = HOTP;
  692. // Counter: required
  693. if (typeof uriParams.counter !== "undefined" && INTEGER_REGEX.test(uriParams.counter)) {
  694. config.counter = parseInt(uriParams.counter, 10);
  695. } else {
  696. throw new TypeError("Missing or invalid 'counter' parameter");
  697. }
  698. } else if (uriType === "totp") {
  699. OTP = TOTP;
  700. // Period: optional
  701. if (typeof uriParams.period !== "undefined") {
  702. if (POSITIVE_INTEGER_REGEX.test(uriParams.period)) {
  703. config.period = parseInt(uriParams.period, 10);
  704. } else {
  705. throw new TypeError("Invalid 'period' parameter");
  706. }
  707. }
  708. } else {
  709. throw new TypeError("Unknown OTP type");
  710. }
  711. // Label: required
  712. // Issuer: optional
  713. if (typeof uriParams.issuer !== "undefined") {
  714. config.issuer = uriParams.issuer;
  715. }
  716. if (uriLabel.length === 2) {
  717. config.label = uriLabel[1];
  718. if (typeof config.issuer === "undefined" || config.issuer === "") {
  719. config.issuer = uriLabel[0];
  720. } else if (uriLabel[0] === "") {
  721. config.issuerInLabel = false;
  722. }
  723. } else {
  724. config.label = uriLabel[0];
  725. if (typeof config.issuer !== "undefined" && config.issuer !== "") {
  726. config.issuerInLabel = false;
  727. }
  728. }
  729. // Secret: required
  730. if (typeof uriParams.secret !== "undefined" && SECRET_REGEX.test(uriParams.secret)) {
  731. config.secret = uriParams.secret;
  732. } else {
  733. throw new TypeError("Missing or invalid 'secret' parameter");
  734. }
  735. // Algorithm: optional
  736. if (typeof uriParams.algorithm !== "undefined") {
  737. if (ALGORITHM_REGEX.test(uriParams.algorithm)) {
  738. config.algorithm = uriParams.algorithm;
  739. } else {
  740. throw new TypeError("Invalid 'algorithm' parameter");
  741. }
  742. }
  743. // Digits: optional
  744. if (typeof uriParams.digits !== "undefined") {
  745. if (POSITIVE_INTEGER_REGEX.test(uriParams.digits)) {
  746. config.digits = parseInt(uriParams.digits, 10);
  747. } else {
  748. throw new TypeError("Invalid 'digits' parameter");
  749. }
  750. }
  751. return new OTP(config);
  752. }
  753. /**
  754. * Converts an HOTP/TOTP object to a Google Authenticator key URI.
  755. * @param {HOTP|TOTP} otp HOTP/TOTP object.
  756. * @returns {string} Google Authenticator Key URI.
  757. */ static stringify(otp) {
  758. if (otp instanceof HOTP || otp instanceof TOTP) {
  759. return otp.toString();
  760. }
  761. throw new TypeError("Invalid 'HOTP/TOTP' object");
  762. }
  763. }
  764. /**
  765. * Library version.
  766. * @type {string}
  767. */ const version = "9.3.1";
  768. export { HOTP, Secret, TOTP, URI, version };