← Runnable Projects

阅读中文版本

TOTP Generator with RFC 6238 Vectors

A dependency-free TOTP implementation using HMAC-SHA1 through the crypto module, verified against the official RFC 6238 appendix test vectors.

intermediatejavascriptunverified

Project

node-totp-demo

Entry point: totp.mjs

Install

node --version

Run

node totp.mjs --time 59

Test

node --test totp.test.mjs

Expected output

Passing --time 59 with the RFC demo secret prints 94287082, matching the RFC 6238 appendix vector exactly.

Dependencies

  • node >=20

Security note

Demo secret is the RFC appendix value; real deployments must store secrets in a vault and never log generated codes.

What it does

Generates 8-digit TOTP codes with HMAC-SHA1 through the crypto module, following RFC 6238. The test suite pins all six official appendix vectors.

import { createHmac } from 'node:crypto';

const DEMO_SECRET = '12345678901234567890';

export function totp(secret, time, { digits = 8, step = 30 } = {}) {
  const counter = Math.floor(time / step);
  const buf = Buffer.alloc(8);
  buf.writeBigUInt64BE(BigInt(counter));
  const mac = createHmac('sha1', Buffer.from(secret, 'ascii')).update(buf).digest();
  const offset = mac[mac.length - 1] & 0x0f;
  const bin =
    ((mac[offset] & 0x7f) << 24) |
    (mac[offset + 1] << 16) |
    (mac[offset + 2] << 8) |
    mac[offset + 3];
  return String(bin % 10 ** digits).padStart(digits, '0');
}

const args = process.argv.slice(2);
let time = Math.floor(Date.now() / 1000);
const timeIndex = args.indexOf('--time');
if (timeIndex !== -1 && args[timeIndex + 1] !== undefined) {
  time = Number(args[timeIndex + 1]);
}
console.log(totp(DEMO_SECRET, time));
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { totp } from './totp.mjs';

const RFC_VECTORS = [
  [59, '94287082'],
  [1111111109, '07081804'],
  [1111111111, '14050471'],
  [1234567890, '89005924'],
  [2000000000, '69279037'],
  [20000000000, '65353130'],
];

test('matches every RFC 6238 appendix vector', () => {
  for (const [t, expected] of RFC_VECTORS) {
    assert.equal(totp('12345678901234567890', t), expected, 'vector failed at t=' + t);
  }
});

test('codes are zero-padded to the digit count', () => {
  assert.match(totp('12345678901234567890', 59, { digits: 6 }), /^\d{6}$/);
});

Run and verify

node --test totp.test.mjs
node totp.mjs --time 59

Pass –time to reproduce a specific RFC vector, or run with no arguments to get the code for the current time.

Expected Output

The test run should print two passing tests, with the first asserting all six RFC 6238 appendix vectors. The demo’s console.log output should print a single 8-digit code; running node totp.mjs --time 59 with the RFC demo secret should print:

94287082

That value is the first appendix vector exactly — HMAC-SHA1 over the 8-byte big-endian counter for time step 1, dynamic truncation, then decimal rendering zero-padded to eight digits. Run without --time, the code changes every 30 seconds with the current time step.

Limitations

No drift window acceptance and no rate limiting on verification attempts; production use needs both plus secret storage in a vault.