totpChecker.js 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import base32Decode from 'base32-decode';
  2. import crypto from 'crypto';
  3. const RFC_4648 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  4. export class TotpChecker {
  5. static ValidateTotp(totpSecret, code, period, digits, algorithm) {
  6. if (!totpSecret && !code)
  7. return true;
  8. if ((!totpSecret && code) || (totpSecret && !code))
  9. return false;
  10. let currentPeriod = TotpChecker.getCurrentPeriod(period);
  11. const input = Buffer.from(code.replace(/[^0-9]/g, "").trim());
  12. return TotpChecker.DoGenerateCode(totpSecret, [currentPeriod - 1, currentPeriod, currentPeriod + 1], digits || 6, algorithm || "SHA-1")
  13. .find(x => {
  14. try {
  15. return crypto.timingSafeEqual(Buffer.from(x), input);
  16. }
  17. catch (err) {
  18. return false;
  19. }
  20. }) !== undefined;
  21. }
  22. static DoGenerateCode(totpSecret, period, digits, algorithm) {
  23. const secretAsBase64 = Buffer.from(base32Decode(totpSecret, "RFC4648"));
  24. return (Array.isArray(period) ? period : [period]).map(i => {
  25. // Encode period as a Buffer
  26. var periodAsBuffer = Buffer.alloc(8);
  27. periodAsBuffer.write((i.toString(16)).padStart(16, '0'), 0, 'hex');
  28. return periodAsBuffer;
  29. }).map(period => {
  30. // Encode period using algorithm and secret
  31. return crypto.createHmac(algorithm, secretAsBase64)
  32. .update(period)
  33. .digest();
  34. }).map(hash => {
  35. // Truncate output hash
  36. let offset = hash[hash.length - 1] & 0xF;
  37. var truncatedHash = ((hash[offset + 0] & 0x7F) << 24 |
  38. (hash[offset + 1] & 0xFF) << 16 |
  39. (hash[offset + 2] & 0xFF) << 8 |
  40. (hash[offset + 3] & 0xFF)) % (10 ** digits);
  41. return (`${truncatedHash}`.padStart(digits, '0'));
  42. });
  43. }
  44. static getCurrentPeriod(period) {
  45. return Math.floor(Date.now() / ((period || 30) * 1000));
  46. }
  47. static GenerateCode(totpSecret, period, digits, algorithm) {
  48. return TotpChecker.DoGenerateCode(totpSecret, this.getCurrentPeriod(period), digits || 6, algorithm || "SHA-1").shift();
  49. }
  50. static GenerateSecret(secretLength) {
  51. secretLength = secretLength || 13;
  52. return Array.from(crypto.randomBytes(secretLength)).map(x => RFC_4648[x % RFC_4648.length]).join("");
  53. }
  54. static GenerateUrl(optionsOrIssuer) {
  55. let options = typeof optionsOrIssuer === "string" ? { issuer: optionsOrIssuer } : optionsOrIssuer;
  56. options.digits = options.digits || 6;
  57. options.period = options.period || 30;
  58. options.algorithm = options.algorithm || "SHA-1";
  59. options.label = encodeURIComponent(options.label || options.issuer);
  60. const secretStr = options.secret || TotpChecker.GenerateSecret(options.secretLength || 13);
  61. return {
  62. url: `otpauth://totp/${options.issuer}?issuer=${options.issuer}&secret=${secretStr}&digits=${options.digits}&period=${options.period}&algorithm=${options.algorithm}`,
  63. secret: secretStr
  64. };
  65. }
  66. }
  67. //# sourceMappingURL=totpChecker.js.map