Skip to content

Commit

Permalink
[ Request ] Replace Buffer with equivalent Uint8Array and crypto
Browse files Browse the repository at this point in the history
…with Web Crypto (#74)

This makes the package fully "serverless" compatible.
Author: @bitofbreeze
  • Loading branch information
bitofbreeze authored Sep 10, 2024
1 parent d3fe144 commit 7f1cdbb
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 13 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20

- name: Install Dependencies
uses: bahmutov/[email protected]
Expand All @@ -47,7 +47,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20

- name: Install Dependencies
uses: bahmutov/[email protected]
Expand All @@ -71,7 +71,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20

- name: Install Dependencies
uses: bahmutov/[email protected]
Expand All @@ -95,7 +95,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20

- name: Install Dependencies
uses: bahmutov/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
registry-url: https://registry.npmjs.org/

- name: Install Dependencies
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"/public/dist"
],
"dependencies": {
"@epic-web/totp": "^1.1.3",
"@epic-web/totp": "^2.0.0",
"base32-encode": "^2.0.0",
"jose": "^5.8.0"
},
Expand All @@ -62,5 +62,8 @@
"vite": "^4.5.3",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0"
},
"engines": {
"node": ">=20"
}
}
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
private readonly sendTOTP: SendTOTP
private readonly validateEmail: ValidateEmail
private readonly _totpGenerationDefaults = {
algorithm: 'SHA256', // More secure than SHA1
algorithm: 'SHA-256', // More secure than SHA1
charSet: 'abcdefghijklmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789', // No O or 0
digits: 6,
period: 60,
Expand Down Expand Up @@ -438,7 +438,7 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
const isValidEmail = await this.validateEmail(email)
if (!isValidEmail) throw new Error(this.customErrors.invalidEmail)

const { otp: code, secret } = generateTOTP({
const { otp: code, secret } = await generateTOTP({
...this.totpGeneration,
secret: this.totpGeneration.secret ?? generateSecret(),
})
Expand Down Expand Up @@ -504,7 +504,7 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
if (Date.now() - totpData.createdAt > this.totpGeneration.period * 1000) {
throw new Error(this.customErrors.expiredTotp)
}
if (!verifyTOTP({ ...this.totpGeneration, secret: totpData.secret, otp: code })) {
if (!await verifyTOTP({ ...this.totpGeneration, secret: totpData.secret, otp: code })) {
throw new Error(this.customErrors.invalidTotp)
}
} catch (error) {
Expand Down
55 changes: 51 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { TOTPData, TOTPSessionData } from './index.js'
import type { AuthenticateOptions } from 'remix-auth'
import { ERRORS } from './constants.js'

import base32Encode from 'base32-encode'
import * as crypto from 'node:crypto'

/**
* TOTP Generation.
*/
export function generateSecret() {
return base32Encode(crypto.randomBytes(32), 'RFC4648').toString() as string
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
return base32Encode(randomBytes, 'RFC4648').toString() as string
}

export function generateMagicLink(options: {
Expand All @@ -24,14 +24,61 @@ export function generateMagicLink(options: {
return url.toString()
}

// https://github.com/sindresorhus/uint8array-extras/blob/main/index.js#L222
const hexToDecimalLookupTable = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
a: 10,
b: 11,
c: 12,
d: 13,
e: 14,
f: 15,
A: 10,
B: 11,
C: 12,
D: 13,
E: 14,
F: 15,
};
function hexToUint8Array(hexString: string) {
if (hexString.length % 2 !== 0) {
throw new Error('Invalid Hex string length.');
}

const resultLength = hexString.length / 2;
const bytes = new Uint8Array(resultLength);

for (let index = 0; index < resultLength; index++) {
const highNibble = hexToDecimalLookupTable[hexString[index * 2] as keyof typeof hexToDecimalLookupTable];
const lowNibble = hexToDecimalLookupTable[hexString[(index * 2) + 1] as keyof typeof hexToDecimalLookupTable];

if (highNibble === undefined || lowNibble === undefined) {
throw new Error(`Invalid Hex character encountered at position ${index * 2}`);
}

bytes[index] = (highNibble << 4) | lowNibble;
}

return bytes;
}

/**
* Miscellaneous.
*/
export function asJweKey(secret: string) {
if (!/^[0-9a-fA-F]{64}$/.test(secret)) {
throw new Error('Secret must be a string with 64 hex characters.')
}
return Buffer.from(secret, 'hex')
return hexToUint8Array(secret)
}

export function coerceToOptionalString(value: unknown) {
Expand Down

0 comments on commit 7f1cdbb

Please sign in to comment.