├── .eslintignore ├── .github ├── FUNDING.yml └── workflows │ └── publish.yaml ├── .gitignore ├── docs ├── favicon.ico ├── og-logo.jpg ├── pages │ ├── reference │ │ ├── index.md │ │ └── main │ │ │ ├── generateHOTP.md │ │ │ ├── generateTOTP.md │ │ │ ├── verifyHOTP.md │ │ │ ├── verifyTOTP.md │ │ │ ├── index.md │ │ │ ├── createHOTPKeyURI.md │ │ │ ├── createTOTPKeyURI.md │ │ │ └── verifyTOTPWithGracePeriod.md │ ├── index.md │ └── examples │ │ ├── hotp.md │ │ └── totp.md ├── malta.config.json └── logo.svg ├── .prettierrc.json ├── .prettierignore ├── tsconfig.build.json ├── src ├── index.ts ├── hotp.test.ts ├── hotp.ts └── totp.ts ├── CHANGELOG.md ├── tsconfig.json ├── README.md ├── .eslintrc.cjs ├── LICENSE ├── package.json └── CONTRIBUTING.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/** -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pilcrowOnPaper 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | dist 3 | node_modules 4 | package-lock.json -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslo-project/otp/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/og-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslo-project/otp/HEAD/docs/og-logo.jpg -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "trailingComma": "none", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | pnpm-lock.yaml 6 | package-lock.json 7 | yarn.lock 8 | 9 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["src/**/*.test.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/pages/reference/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "API reference" 3 | --- 4 | 5 | # API reference 6 | 7 | ## Modules 8 | 9 | - [`@oslojs/otp`](/reference/main) 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { generateHOTP, verifyHOTP, createHOTPKeyURI } from "./hotp.js"; 2 | export { generateTOTP, verifyTOTP, verifyTOTPWithGracePeriod, createTOTPKeyURI } from "./totp.js"; 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @oslojs/otp 2 | 3 | ## 1.1.0 4 | 5 | - Add `verifyTOTPWithGracePeriod()`. 6 | 7 | ## 1.0.0 8 | 9 | - No changes. 10 | 11 | ## 0.2.2 12 | 13 | - Update README 14 | 15 | ## 0.2.1 16 | 17 | - Update dependencies. 18 | -------------------------------------------------------------------------------- /docs/pages/reference/main/generateHOTP.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "generateHOTP()" 3 | --- 4 | 5 | Generates a new HOTP with SHA-1. 6 | 7 | ## Definition 8 | 9 | ```ts 10 | function generateHOTP(key: Uint8Array, counter: bigint, digits: number): string; 11 | ``` 12 | 13 | ### Parameters 14 | 15 | - `key`: HMAC key 16 | - `counter` 17 | - `digits` 18 | -------------------------------------------------------------------------------- /docs/pages/reference/main/generateTOTP.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "generateTOTP()" 3 | --- 4 | 5 | Generates a new TOTP with SHA-1. 6 | 7 | ## Definition 8 | 9 | ```ts 10 | function generateTOTP(key: Uint8Array, intervalInSeconds: number, digits: number): string; 11 | ``` 12 | 13 | ### Parameters 14 | 15 | - `key`: HMAC key 16 | - `intervalInSeconds` 17 | - `digits` 18 | -------------------------------------------------------------------------------- /docs/pages/reference/main/verifyHOTP.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "verifyHOTP()" 3 | --- 4 | 5 | Verifies an HOTP with constant-time comparison. 6 | 7 | ## Definition 8 | 9 | ```ts 10 | function verifyHOTP(key: Uint8Array, counter: bigint, digits: number, otp: string): boolean; 11 | ``` 12 | 13 | ### Parameters 14 | 15 | - `key`: HMAC key 16 | - `counter` 17 | - `digits` 18 | - `otp` 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es2022", 8 | "verbatimModuleSyntax": true, 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "moduleDetection": "force", 12 | "strict": true, 13 | "moduleResolution": "NodeNext", 14 | "module": "NodeNext" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/pages/reference/main/verifyTOTP.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "verifyTOTP()" 3 | --- 4 | 5 | Verifies a TOTP with constant-time comparison. 6 | 7 | ## Definition 8 | 9 | ```ts 10 | function verifyTOTP( 11 | key: Uint8Array, 12 | intervalInSeconds: number, 13 | digits: number, 14 | otp: string 15 | ): boolean; 16 | ``` 17 | 18 | ### Parameters 19 | 20 | - `key`: HMAC key 21 | - `intervalInSeconds` 22 | - `digits` 23 | - `otp` 24 | -------------------------------------------------------------------------------- /docs/pages/reference/main/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@oslojs/otp" 3 | --- 4 | 5 | # @oslojs/otp 6 | 7 | ## Functions 8 | 9 | - [`createHOTPKeyURI()`](/reference/main/createHOTPKeyURI) 10 | - [`createTOTPKeyURI()`](/reference/main/createTOTPKeyURI) 11 | - [`generateHOTP()`](/reference/main/generateHOTP) 12 | - [`generateTOTP()`](/reference/main/generateTOTP) 13 | - [`verifyHOTP()`](/reference/main/verifyHOTP) 14 | - [`verifyTOTP()`](/reference/main/verifyTOTP) 15 | - [`verifyTOTPWithGracePeriod()`](/reference/main/verifyTOTPWithGracePeriod) 16 | -------------------------------------------------------------------------------- /docs/pages/reference/main/createHOTPKeyURI.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "createHOTPKeyURI()" 3 | --- 4 | 5 | Creates an HOTP key URI with the algorithm parameter set to `SHA1`. 6 | 7 | ## Definition 8 | 9 | ```ts 10 | function createHOTPKeyURI( 11 | issuer: string, 12 | accountName: string, 13 | key: Uint8Array, 14 | counter: bigint, 15 | digits: number 16 | ): string; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `issuer` 22 | - `key` 23 | - `key` 24 | - `counter` 25 | - `digits` 26 | 27 | ## Example 28 | 29 | ```ts 30 | import { createHOTPKeyURI } from "@oslojs/otp"; 31 | 32 | const uri = createHOTPKeyURI("My App", "user@example.com", key, counter, 6); 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/pages/reference/main/createTOTPKeyURI.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "createTOTPKeyURI()" 3 | --- 4 | 5 | Creates an TOTP key URI with the algorithm parameter set to `SHA1`. 6 | 7 | ## Definition 8 | 9 | ```ts 10 | function createTOTPKeyURI( 11 | issuer: string, 12 | accountName: string, 13 | key: Uint8Array, 14 | periodInSeconds: number, 15 | digits: number 16 | ): string; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `issuer` 22 | - `accountName` 23 | - `key` 24 | - `periodInSeconds` 25 | - `digits` 26 | 27 | ## Example 28 | 29 | ```ts 30 | import { createTOTPKeyURI } from "@oslojs/otp"; 31 | 32 | const uri = createTOTPKeyURI("My App", "user@example.com", key, 30, 6); 33 | ``` 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @oslojs/otp 2 | 3 | **Documentation: https://otp.oslojs.dev** 4 | 5 | A JavaScript library for generating and verifying OTPs by [Oslo](https://oslojs.dev). 6 | 7 | Supports HMAC-based one-time passwords (HOTP) and time-based one-time passwords (TOTP) as defined in [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226) and [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238). 8 | 9 | - Runtime-agnostic 10 | - No third-party dependencies 11 | - Fully typed 12 | 13 | ```ts 14 | import { generateTOTP, verifyTOTP } from "@oslojs/otp"; 15 | 16 | const totp = generateTOTP(key, 30, 6); 17 | const valid = verifyTOTP(totp, key, 30, 6); 18 | ``` 19 | 20 | ## Installation 21 | 22 | ``` 23 | npm i @oslojs/otp 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/pages/reference/main/verifyTOTPWithGracePeriod.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "verifyTOTPWithGracePeriod()" 3 | --- 4 | 5 | # verifyTOTPWithGracePeriod() 6 | 7 | Verifies a TOTP using [`verifyTOTP()`](/reference/main/verifyTOTP) with a grace period. If the grace period is 30 seconds for example, the OTP is valid if it was generated within the 30-second time span before or after the current machine time (60 seconds in total). 8 | 9 | ```ts 10 | function verifyTOTPWithGracePeriod( 11 | key: Uint8Array, 12 | intervalInSeconds: number, 13 | digits: number, 14 | otp: string, 15 | gracePeriodInSeconds: number 16 | ): boolean; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `key`: HMAC key 22 | - `intervalInSeconds` 23 | - `digits` 24 | - `otp` 25 | - `gracePeriodInSeconds` 26 | -------------------------------------------------------------------------------- /docs/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@oslojs/otp documentation" 3 | --- 4 | 5 | # @oslojs/otp documentation 6 | 7 | A JavaScript library for generating and verifying OTPs by [Oslo](https://oslojs.dev). 8 | 9 | Supports HMAC-based one-time passwords (HOTP) and time-based one-time passwords (TOTP) using SHA-1 as defined in [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226) and [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238). 10 | 11 | - Runtime-agnostic 12 | - No third-party dependencies 13 | - Fully typed 14 | 15 | ```ts 16 | import { generateTOTP, verifyTOTP } from "@oslojs/otp"; 17 | 18 | const totp = generateTOTP(key, 30, 6); 19 | const valid = verifyTOTP(key, 30, 6, totp); 20 | ``` 21 | 22 | ## Installation 23 | 24 | ``` 25 | npm i @oslojs/otp 26 | ``` 27 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "@typescript-eslint/no-explicit-any": "off", 4 | "@typescript-eslint/no-empty-function": "off", 5 | "@typescript-eslint/ban-types": "off", 6 | "@typescript-eslint/no-unused-vars": [ 7 | "error", 8 | { 9 | argsIgnorePattern: "^_", 10 | varsIgnorePattern: "^_", 11 | caughtErrorsIgnorePattern: "^_" 12 | } 13 | ], 14 | "@typescript-eslint/no-empty-interface": "off", 15 | "@typescript-eslint/explicit-function-return-type": "error", 16 | "no-async-promise-executor": "off", 17 | "no-useless-catch": "off" 18 | }, 19 | parser: "@typescript-eslint/parser", 20 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 21 | plugins: ["@typescript-eslint"], 22 | env: { 23 | node: true 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /docs/malta.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oslojs/otp", 3 | "description": "One-time passwords with HOTP and TOTP.", 4 | "domain": "https://otp.oslojs.dev", 5 | "twitter": "@pilcrowonpaper", 6 | "asset_hashing": true, 7 | "sidebar": [ 8 | { 9 | "title": "Examples", 10 | "pages": [ 11 | ["HMAC-based one-time passwords", "/examples/hotp"], 12 | ["Time-based one-time passwords", "/examples/totp"] 13 | ] 14 | }, 15 | { 16 | "title": "API reference", 17 | "pages": [["@oslojs/otp", "/reference/main"]] 18 | }, 19 | { 20 | "title": "Links", 21 | "pages": [ 22 | ["GitHub", "https://github.com/oslo-project/otp"], 23 | ["Oslo", "https://oslojs.dev"], 24 | ["Twitter", "https://twitter.com/pilcrowonpaper"], 25 | ["Donate", "https://github.com/sponsors/pilcrowOnPaper"] 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /docs/pages/examples/hotp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "HMAC-based one-time passwords" 3 | --- 4 | 5 | # HMAC-based one-time passwords 6 | 7 | Use [`generateHOTP()`](/reference/main/generateHOTP) and [`verifyHOTP()`](/reference/main/verifyHOTP) to generate and verify HOTPs. 8 | 9 | ```ts 10 | import { generateHOTP, verifyHOTP } from "@oslojs/otp"; 11 | 12 | const digits = 6; 13 | let counter = 10n; 14 | 15 | const otp = generateHOTP(key, counter, digits); 16 | const validOTP = verifyHOTP(key, counter, digits, otp); 17 | ``` 18 | 19 | Use [`createHOTPKeyURI()`](/reference/main/createHOTPKeyURI) to create a key URI, which are then usually encoded into a QR code. 20 | 21 | ```ts 22 | import { createHOTPKeyURI } from "@oslojs/otp"; 23 | 24 | const issuer = "My app"; 25 | const accountName = "user@example.com"; 26 | const digits = 6; 27 | const uri = createHOTPKeyURI(issuer, accountName, key, counter, digits); 28 | ``` 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: "Publish" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | env: 8 | CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_PAGES_API_TOKEN}} 9 | 10 | jobs: 11 | publish: 12 | name: Publish 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: setup actions 16 | uses: actions/checkout@v3 17 | - name: setup node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 20.5.1 21 | registry-url: https://registry.npmjs.org 22 | - name: install malta 23 | working-directory: docs 24 | run: | 25 | curl -o malta.tgz -L https://github.com/pilcrowonpaper/malta/releases/latest/download/linux-amd64.tgz 26 | tar -xvzf malta.tgz 27 | - name: build 28 | working-directory: docs 29 | run: ./linux-amd64/malta build 30 | - name: install wrangler 31 | run: npm i -g wrangler 32 | - name: deploy 33 | run: wrangler pages deploy docs/dist --project-name oslo-otp --branch main 34 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 pilcrowOnPaper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oslojs/otp", 3 | "type": "module", 4 | "version": "1.1.0", 5 | "description": "One-time passwords with HOTP and TOTP", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "module": "dist/index.js", 9 | "scripts": { 10 | "build": "rm -rf dist/* && tsc --project tsconfig.build.json", 11 | "format": "prettier -w .", 12 | "lint": "eslint src", 13 | "test": "vitest run --sequence.concurrent" 14 | }, 15 | "files": [ 16 | "/dist/" 17 | ], 18 | "keywords": [ 19 | "auth", 20 | "otp", 21 | "hotp", 22 | "totp" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/oslo-project/otp" 27 | }, 28 | "author": "pilcrowOnPaper", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@types/node": "^20.8.6", 32 | "@typescript-eslint/eslint-plugin": "^6.7.5", 33 | "@typescript-eslint/parser": "^6.7.5", 34 | "auri": "^2.0.0", 35 | "eslint": "^8.51.0", 36 | "prettier": "^3.0.3", 37 | "typescript": "^5.2.2", 38 | "vitest": "^0.34.6" 39 | }, 40 | "dependencies": { 41 | "@oslojs/binary": "1.0.0", 42 | "@oslojs/crypto": "1.0.0", 43 | "@oslojs/encoding": "1.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/pages/examples/totp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Time-based one-time passwords" 3 | --- 4 | 5 | # Time-based one-time passwords 6 | 7 | Use [`generateTOTP()`](/reference/main/generateTOTP) to generate TOTPs and use [`verifyTOTPWithGracePeriod()`](/reference/main/verifyTOTPWithGracePeriod) or [`verifyTOTP()`](/reference/main/verifyTOTP) to verify them. Adding a grace period allows you to account for network latency and time discrepancy between devices. 8 | 9 | ```ts 10 | import { generateTOTP, verifyTOTPWithGracePeriod, verifyTOTP } from "@oslojs/otp"; 11 | 12 | const digits = 6; 13 | const intervalInSeconds = 30; 14 | 15 | const otp = generateTOTP(key, intervalInSeconds, digits); 16 | const valid = verifyTOTPWithGracePeriod(key, intervalInSeconds, digits, otp, 30); 17 | const valid = verifyTOTP(key, intervalInSeconds, digits, otp); 18 | ``` 19 | 20 | Use [`createTOTPKeyURI()`](/reference/main/createTOTPKeyURI) to create a key URI, which are then usually encoded into a QR code. 21 | 22 | ```ts 23 | import { createTOTPKeyURI } from "@oslojs/otp"; 24 | 25 | const issuer = "My app"; 26 | const accountName = "user@example.com"; 27 | const intervalInSeconds = 30; 28 | const digits = 6; 29 | const uri = createTOTPKeyURI(issuer, accountName, key, intervalInSeconds, digits); 30 | ``` 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor manual 2 | 3 | ## Contributing to the docs 4 | 5 | We welcome all contributions to the docs, especially grammar fixes. Oslo uses [Malta](https://malta-cli.pages.dev) for generating documentation sites. All pages are markdown files located in the `docs/pages` directory. Make sure to update `malta.config.json` if you need a page to appear in the sidebar. 6 | 7 | ## Contributing to the source code 8 | 9 | We are open to most contributions, but please open a new issue before creating a pull request, especially for new features. It's likely your PR will be rejected if not. We have intentionally limited the scope of the project and we would like to keep the package lean. 10 | 11 | ### Set up 12 | 13 | Install dependencies with PNPM. 14 | 15 | ``` 16 | pnpm i 17 | ``` 18 | 19 | ### Testing 20 | 21 | Run `pnpm test` to run tests and `pnpm build` to build the package. 22 | 23 | ``` 24 | pnpm test 25 | 26 | pnpm build 27 | ``` 28 | 29 | ### Creating changesets 30 | 31 | When creating a PR, create a changeset with `pnpm auri add`. If you made multiple changes, create multiple changesets. Use `minor` for new features, and use `patch` for bug fixes: 32 | 33 | ``` 34 | pnpm auri add minor 35 | pnpm auri add patch 36 | ``` 37 | 38 | A new markdown file should be created in `.changesets` directory. Write a short summary of the change: 39 | 40 | ``` 41 | Fix: Handle negative numbers in `sqrt()` 42 | ``` 43 | 44 | ``` 45 | Feat: Add `greet()` 46 | ``` 47 | -------------------------------------------------------------------------------- /src/hotp.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "vitest"; 2 | import { test } from "vitest"; 3 | import { generateHOTP, verifyHOTP } from "./hotp.js"; 4 | 5 | const secret = new Uint8Array([ 6 | 0x63, 0x07, 0x87, 0x06, 0xe4, 0x89, 0x1b, 0x07, 0x85, 0xba, 0x42, 0xbd, 0x23, 0xac, 0xdd, 0x09, 7 | 0xe4, 0x69, 0x33, 0x63, 0xbe, 0xfa, 0x25, 0xa4, 0x13, 0x46, 0xee, 0x0b, 0xda, 0xb0, 0x72, 0x4c, 8 | 0xa0, 0x8f, 0x8d, 0x26, 0x63, 0x0e, 0xb5, 0x6c, 0xa3, 0xfd, 0xce, 0x6c, 0xc0, 0x0e, 0xf8, 0x65, 9 | 0x6d, 0x1f, 0xeb, 0xc7, 0x35, 0x92, 0x87, 0x16, 0x3d, 0x11, 0x34, 0x20, 0x00, 0x7a, 0x18, 0x1c 10 | ]); 11 | 12 | test("generateHOTP()", () => { 13 | expect(generateHOTP(secret, 0n, 6)).toBe("173573"); 14 | expect(generateHOTP(secret, 10n, 6)).toBe("110880"); 15 | expect(generateHOTP(secret, 100n, 6)).toBe("020803"); 16 | expect(generateHOTP(secret, 1000n, 6)).toBe("115716"); 17 | }); 18 | 19 | test("verifyHOTP()", () => { 20 | expect(verifyHOTP(secret, 0n, 6, "173573")).toBe(true); 21 | expect(verifyHOTP(secret, 0n, 6, "000000")).toBe(false); 22 | expect(verifyHOTP(secret, 10n, 6, "110880")).toBe(true); 23 | expect(verifyHOTP(secret, 10n, 6, "000000")).toBe(false); 24 | expect(verifyHOTP(secret, 100n, 6, "020803")).toBe(true); 25 | expect(verifyHOTP(secret, 100n, 6, "000000")).toBe(false); 26 | expect(verifyHOTP(secret, 1000n, 6, "115716")).toBe(true); 27 | expect(verifyHOTP(secret, 1000n, 6, "000000")).toBe(false); 28 | 29 | expect(verifyHOTP(secret, 0n, 8, "173573")).toBe(false); 30 | }); 31 | -------------------------------------------------------------------------------- /src/hotp.ts: -------------------------------------------------------------------------------- 1 | import { bigEndian } from "@oslojs/binary"; 2 | import { hmac } from "@oslojs/crypto/hmac"; 3 | import { SHA1 } from "@oslojs/crypto/sha1"; 4 | import { constantTimeEqual } from "@oslojs/crypto/subtle"; 5 | import { encodeBase32NoPadding } from "@oslojs/encoding"; 6 | 7 | export function generateHOTP(key: Uint8Array, counter: bigint, digits: number): string { 8 | if (digits < 6 || digits > 8) { 9 | throw new TypeError("Digits must be between 6 and 8"); 10 | } 11 | const counterBytes = new Uint8Array(8); 12 | bigEndian.putUint64(counterBytes, counter, 0); 13 | const HS = hmac(SHA1, key, counterBytes); 14 | const offset = HS[HS.byteLength - 1] & 0x0f; 15 | const truncated = HS.slice(offset, offset + 4); 16 | truncated[0] &= 0x7f; 17 | const SNum = bigEndian.uint32(truncated, 0); 18 | const D = SNum % 10 ** digits; 19 | return D.toString().padStart(digits, "0"); 20 | } 21 | 22 | export function verifyHOTP(key: Uint8Array, counter: bigint, digits: number, otp: string): boolean { 23 | if (digits < 6 || digits > 8) { 24 | throw new TypeError("Digits must be between 6 and 8"); 25 | } 26 | if (otp.length !== digits) { 27 | return false; 28 | } 29 | const bytes = new TextEncoder().encode(otp); 30 | const expected = generateHOTP(key, counter, digits); 31 | const expectedBytes = new TextEncoder().encode(expected); 32 | const valid = constantTimeEqual(bytes, expectedBytes); 33 | return valid; 34 | } 35 | 36 | export function createHOTPKeyURI( 37 | issuer: string, 38 | accountName: string, 39 | key: Uint8Array, 40 | counter: bigint, 41 | digits: number 42 | ): string { 43 | const encodedIssuer = encodeURIComponent(issuer); 44 | const encodedAccountName = encodeURIComponent(accountName); 45 | const base = `otpauth://hotp/${encodedIssuer}:${encodedAccountName}`; 46 | const params = new URLSearchParams(); 47 | params.set("issuer", issuer); 48 | params.set("algorithm", "SHA1"); 49 | params.set("secret", encodeBase32NoPadding(key)); 50 | params.set("counter", counter.toString()); 51 | params.set("digits", digits.toString()); 52 | return base + "?" + params.toString(); 53 | } 54 | -------------------------------------------------------------------------------- /src/totp.ts: -------------------------------------------------------------------------------- 1 | import { encodeBase32NoPadding } from "@oslojs/encoding"; 2 | import { generateHOTP, verifyHOTP } from "./hotp.js"; 3 | 4 | export function generateTOTP(key: Uint8Array, intervalInSeconds: number, digits: number): string { 5 | if (digits < 6 || digits > 8) { 6 | throw new TypeError("Digits must be between 6 and 8"); 7 | } 8 | const counter = BigInt(Math.floor(Date.now() / (intervalInSeconds * 1000))); 9 | const hotp = generateHOTP(key, counter, digits); 10 | return hotp; 11 | } 12 | 13 | export function verifyTOTP( 14 | key: Uint8Array, 15 | intervalInSeconds: number, 16 | digits: number, 17 | otp: string 18 | ): boolean { 19 | const counter = BigInt(Math.floor(Date.now() / (intervalInSeconds * 1000))); 20 | const valid = verifyHOTP(key, counter, digits, otp); 21 | return valid; 22 | } 23 | 24 | export function verifyTOTPWithGracePeriod( 25 | key: Uint8Array, 26 | intervalInSeconds: number, 27 | digits: number, 28 | otp: string, 29 | gracePeriodInSeconds: number 30 | ): boolean { 31 | if (gracePeriodInSeconds < 0) { 32 | throw new TypeError("Grace period must be a positive number"); 33 | } 34 | const nowUnixMilliseconds = Date.now(); 35 | let counter = BigInt( 36 | Math.floor((nowUnixMilliseconds - gracePeriodInSeconds * 1000) / (intervalInSeconds * 1000)) 37 | ); 38 | const maxCounterInclusive = BigInt( 39 | Math.floor((nowUnixMilliseconds + gracePeriodInSeconds * 1000) / (intervalInSeconds * 1000)) 40 | ); 41 | while (counter <= maxCounterInclusive) { 42 | const valid = verifyHOTP(key, counter, digits, otp); 43 | if (valid) { 44 | return true; 45 | } 46 | counter++; 47 | } 48 | return false; 49 | } 50 | 51 | export function createTOTPKeyURI( 52 | issuer: string, 53 | accountName: string, 54 | key: Uint8Array, 55 | periodInSeconds: number, 56 | digits: number 57 | ): string { 58 | const encodedIssuer = encodeURIComponent(issuer); 59 | const encodedAccountName = encodeURIComponent(accountName); 60 | const base = `otpauth://totp/${encodedIssuer}:${encodedAccountName}`; 61 | const params = new URLSearchParams(); 62 | params.set("issuer", issuer); 63 | params.set("algorithm", "SHA1"); 64 | params.set("secret", encodeBase32NoPadding(key)); 65 | params.set("period", periodInSeconds.toString()); 66 | params.set("digits", digits.toString()); 67 | return base + "?" + params.toString(); 68 | } 69 | --------------------------------------------------------------------------------