otpauth.slim.esm.js 27 KB

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