Skip to content

Commit

Permalink
feat: web crypro api (#143)
Browse files Browse the repository at this point in the history
* Small config to support replit environment (where I develop)

* convert tests to use bun

* remove last jest mention

* format

* Small change to support replit env, better types, removed outdated str syntaxx

* WebCrypto implemented, reflect async in readme, modify tests to work with async

* #IhateTS

* no need for jssha anymore

* Consistent names

* performance optimizations

* readability

* More performance improvement

* Adapt crypto to be static, performance improvement

* Attempt at fixing node compat

* Better docs

* Built-in leftpad func

* Fix prettier, for the love of God, jest just doesn't want 100% coverage

* Should work, but doesn't idk, jest trippin 🤣

* Incorporate minification

* Slight optimizations

* Fix minification

* At least some optimization dunno why I did & 255 there

* Some formatting... I'm doing nothing at this point, last commit...
  • Loading branch information
7heMech authored May 28, 2024
1 parent 2c5c296 commit d64d40e
Show file tree
Hide file tree
Showing 8 changed files with 663 additions and 544 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
.upm
.local
.cache
.DS_STORE
node_modules
dist
npm-debug.log
coverage
lib
dist
lib
3 changes: 3 additions & 0 deletions .replit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
modules = ["nodejs-20:v8-20230920-bd784b9"]
hidden = [".config", "yarn.lock"]
run = "npm run test:coverage"
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ totp-generator lets you generate TOTP tokens from a TOTP key
import { TOTP } from "totp-generator"

// Keys provided must be base32 strings, ie. only containing characters matching (A-Z, 2-7, =).
const { otp, expires } = TOTP.generate("JBSWY3DPEHPK3PXP")
const { otp, expires } = await TOTP.generate("JBSWY3DPEHPK3PXP")

console.log(otp) // prints a 6-digit time-based token based on provided key and current time
```
Expand All @@ -30,19 +30,19 @@ Settings can be provided as an optional second parameter:
```javascript
import { TOTP } from "totp-generator"

const { otp } = TOTP.generate("JBSWY3DPEHPK3PXP", { digits: 8 })
const { otp } = await TOTP.generate("JBSWY3DPEHPK3PXP", { digits: 8 })
console.log(otp) // prints an 8-digit token

const { otp } = TOTP.generate("JBSWY3DPEHPK3PXP", { algorithm: "SHA-512" })
const { otp } = await TOTP.generate("JBSWY3DPEHPK3PXP", { algorithm: "SHA-512" })
console.log(otp) // prints a token created using a different algorithm

const { otp } = TOTP.generate("JBSWY3DPEHPK3PXP", { period: 60 })
const { otp } = await TOTP.generate("JBSWY3DPEHPK3PXP", { period: 60 })
console.log(otp) // prints a token using a 60-second epoch interval

const { otp } = TOTP.generate("JBSWY3DPEHPK3PXP", { timestamp: 1465324707000 })
const { otp } = await TOTP.generate("JBSWY3DPEHPK3PXP", { timestamp: 1465324707000 })
console.log(otp) // prints a token for given time

const { otp } = TOTP.generate("JBSWY3DPEHPK3PXP", {
const { otp } = await TOTP.generate("JBSWY3DPEHPK3PXP", {
digits: 8,
algorithm: "SHA-512",
period: 60,
Expand Down
8 changes: 2 additions & 6 deletions build-after.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
#!/bin/sh

cat >lib/cjs/package.json <<!EOF
{
"type": "commonjs"
}
{"type":"commonjs"}
!EOF

cat >lib/esm/package.json <<!EOF
{
"type": "module"
}
{"type":"module"}
!EOF
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,18 @@
"url": "git@github.com:bellstrand/totp-generator.git"
},
"scripts": {
"test": "jest",
"test": "jest --silent=false",
"test:coverage": "jest --silent=false --coverage",
"test:prettier": "prettier --check '**/*.js'",
"build": "rm -rf lib/* && yarn build:cjs && yarn build:esm && ./build-after.sh",
"build:cjs": "tsc --p tsconfig-esm.json",
"build:esm": "tsc --p tsconfig-cjs.json"
},
"dependencies": {
"jssha": "^3.3.1"
"build:cjs": "tsc -p tsconfig-cjs.json && terser --source-map -cmo lib/cjs/index.js lib/cjs/index.js",
"build:esm": "tsc -p tsconfig-esm.json && terser --source-map -cmo lib/esm/index.js lib/esm/index.js"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"jest": "^29.7.0",
"prettier": "^3.2.4",
"terser": "^5.31.0",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3"
},
Expand Down
94 changes: 43 additions & 51 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,110 +4,102 @@ describe("totp generation", () => {
beforeEach(() => jest.useFakeTimers())
afterEach(() => jest.resetAllMocks())

test("should generate token with date now = 1971", () => {
test("should generate token with date now = 1971", async () => {
jest.setSystemTime(0)
expect(TOTP.generate("JBSWY3DPEHPK3PXP").otp).toEqual("282760")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual(expect.objectContaining({ otp: "282760" }))
})

test("should generate token with date now = 2016", () => {
test("should generate token with date now = 2016", async () => {
jest.setSystemTime(1465324707000)
expect(TOTP.generate("JBSWY3DPEHPK3PXP").otp).toEqual("341128")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual(expect.objectContaining({ otp: "341128" }))
})

test("should generate correct token at the start of the cycle", () => {
test("should generate correct token at the start of the cycle", async () => {
const start = 1665644340000
jest.setSystemTime(start + 1)
expect(TOTP.generate("JBSWY3DPEHPK3PXP").otp).toEqual("886842")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual(expect.objectContaining({ otp: "886842" }))
})

test("should generate correct token at the end of the cycle", () => {
test("should generate correct token at the end of the cycle", async () => {
const start = 1665644340000
jest.setSystemTime(start - 1)
expect(TOTP.generate("JBSWY3DPEHPK3PXP").otp).toEqual("134996")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual(expect.objectContaining({ otp: "134996" }))
})

test("should generate token with a leading zero", () => {
test("should generate token with a leading zero", async () => {
jest.setSystemTime(1365324707000)
expect(TOTP.generate("JBSWY3DPEHPK3PXP").otp).toEqual("089029")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual(expect.objectContaining({ otp: "089029" }))
})

test("should generate token from a padded base32 key", () => {
test("should generate token from a padded base32 key", async () => {
jest.setSystemTime(1465324707000)
expect(TOTP.generate("CI2FM6EQCI2FM6EQKU======").otp).toEqual("984195")
await expect(TOTP.generate("CI2FM6EQCI2FM6EQKU======")).resolves.toEqual(expect.objectContaining({ otp: "984195" }))
})

test("should throw if key contains an invalid character", () => {
jest.setSystemTime(1465324707000)
expect(() => TOTP.generate("JBSWY3DPEHPK3!@#")).toThrow("Invalid base32 character in key")
test("should throw if key contains an invalid character", async () => {
await expect(TOTP.generate("ABSWY3DPEHPK3!@#")).rejects.toThrow("Invalid base32 character in key")
})

test("should generate longer-lasting token with date now = 2016", () => {
test("should generate longer-lasting token with date now = 2016", async () => {
jest.setSystemTime(1465324707000)
expect(TOTP.generate("JBSWY3DPEHPK3PXP", { period: 60 }).otp).toEqual("313995")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP", { period: 60 })).resolves.toEqual(expect.objectContaining({ otp: "313995" }))
})

test("should generate longer token with date now = 2016", () => {
test("should generate longer token with date now = 2016", async () => {
jest.setSystemTime(1465324707000)
expect(TOTP.generate("JBSWY3DPEHPK3PXP", { digits: 8 }).otp).toEqual("43341128")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP", { digits: 8 })).resolves.toEqual(expect.objectContaining({ otp: "43341128" }))
})

test("should generate SHA-512-based token with date now = 2016", () => {
test("should generate SHA-512-based token with date now = 2016", async () => {
jest.setSystemTime(1465324707000)
expect(TOTP.generate("JBSWY3DPEHPK3PXP", { algorithm: "SHA-512" }).otp).toEqual("093730")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP", { algorithm: "SHA-512" })).resolves.toEqual(expect.objectContaining({ otp: "093730" }))
})

test.each([
{ algorithm: "SHA-1", expected: "341128" },
{ algorithm: "SHA-224", expected: "991776" },
{ algorithm: "SHA-256", expected: "461529" },
{ algorithm: "SHA-384", expected: "988682" },
{ algorithm: "SHA-512", expected: "093730" },
{ algorithm: "SHA3-224", expected: "045085" },
{ algorithm: "SHA3-256", expected: "255060" },
{ algorithm: "SHA3-384", expected: "088901" },
{ algorithm: "SHA3-512", expected: "542105" },
] as { algorithm: TOTPAlgorithm; expected: string }[])("should generate token based on %p algorithm", ({ algorithm, expected }) => {
] as { algorithm: TOTPAlgorithm; expected: string }[])("should generate token based on %p algorithm", async ({ algorithm, expected }) => {
jest.setSystemTime(1465324707000)
expect(TOTP.generate("JBSWY3DPEHPK3PXP", { algorithm }).otp).toEqual(expected)
await expect(TOTP.generate("JBSWY3DPEHPK3PXP", { algorithm })).resolves.toEqual(expect.objectContaining({ otp: expected }))
})

test("should generate token with timestamp from options", () => {
expect(TOTP.generate("JBSWY3DPEHPK3PXP", { timestamp: 1465324707000 }).otp).toEqual("341128")
test("should generate token with timestamp from options", async () => {
await expect(TOTP.generate("JBSWY3DPEHPK3PXP", { timestamp: 1465324707000 })).resolves.toEqual(expect.objectContaining({ otp: "341128" }))
})

test("should return all values when values is less then digits", () => {
test("should return all values when values is less then digits", async () => {
jest.setSystemTime(1634193300000)
expect(TOTP.generate("3IS523AYRNFUE===", { digits: 9 }).otp).toEqual("97859470")
await expect(TOTP.generate("3IS523AYRNFUE===", { digits: 9 })).resolves.toEqual(expect.objectContaining({ otp: "97859470" }))
})

test("should trigger leftpad fix", () => {
test("should trigger leftpad fix", async () => {
jest.setSystemTime(12312354132421332222222222)
expect(TOTP.generate("JBSWY3DPEHPK3PXP").otp).toEqual("895896")
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual(expect.objectContaining({ otp: "895896" }))
})

test("should trigger leftpad fix", () => {
jest.mock("jssha", () => ({
__esModule: true,
default: "mockedDefaultExport",
namedExport: jest.fn(),
}))
jest.setSystemTime(12312354132421332222222222)
expect(TOTP.generate("JBSWY3DPEHPK3PXP").otp).toEqual("895896")
})

test("should generate token with correct expires", () => {
test("should generate token with correct expires", async () => {
const start = 1665644340000
jest.setSystemTime(start - 1)
expect(TOTP.generate("JBSWY3DPEHPK3PXP")).toEqual({ otp: "134996", expires: start })
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual({ otp: "134996", expires: start })
jest.setSystemTime(start)
expect(TOTP.generate("JBSWY3DPEHPK3PXP")).toEqual({ otp: "886842", expires: start + 30000 })
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual({ otp: "886842", expires: start + 30000 })
jest.setSystemTime(start + 1)
expect(TOTP.generate("JBSWY3DPEHPK3PXP")).toEqual({ otp: "886842", expires: start + 30000 })
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual({ otp: "886842", expires: start + 30000 })
jest.setSystemTime(start + 29999)
expect(TOTP.generate("JBSWY3DPEHPK3PXP")).toEqual({ otp: "886842", expires: start + 30000 })
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual({ otp: "886842", expires: start + 30000 })
jest.setSystemTime(start + 30000)
expect(TOTP.generate("JBSWY3DPEHPK3PXP")).toEqual({ otp: "421127", expires: start + 60000 })
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual({ otp: "421127", expires: start + 60000 })
jest.setSystemTime(start + 30001)
expect(TOTP.generate("JBSWY3DPEHPK3PXP")).toEqual({ otp: "421127", expires: start + 60000 })
await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual({ otp: "421127", expires: start + 60000 })
})

test("uses node crypto as a fallback", async () => {
jest.setSystemTime(0)

Object.defineProperty(globalThis, "crypto", { value: undefined, writable: true })

await expect(TOTP.generate("JBSWY3DPEHPK3PXP")).resolves.toEqual(expect.objectContaining({ otp: "282760" }))
})
})
Loading

0 comments on commit d64d40e

Please sign in to comment.