Skip to content

Commit

Permalink
Merge pull request #46 from kimitrii/kimitri
Browse files Browse the repository at this point in the history
Add support to select algorithm for token generation on Totp lib
  • Loading branch information
kimitrii authored Jan 26, 2025
2 parents 17542c9 + ca1ce1c commit 5449e8d
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 19 deletions.
49 changes: 37 additions & 12 deletions src/lib/totp/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,47 @@
import crypto from 'node:crypto'

export class Totp {
public generateSecret({ name }: { name: string }): {
public generateSecret({
service,
user,
algorithm
}: {
service: string
user: string
algorithm: 'SHA1' | 'SHA256' | 'SHA512'
}): {
secret?: string
otpauthUrl?: string
error?: string
} {
const urlParamRegex = /^[A-Za-z0-9_-]+$/
if (!urlParamRegex.test(name)) {
return { error: 'Invalid name.' }
if (!urlParamRegex.test(user)) {
return { error: 'Invalid user.' }
}
if (!urlParamRegex.test(service)) {
return { error: 'Invalid service.' }
}

const secret = this.generateTOTPSecret()
const otpauthUrl = `otpauth://totp/${name}?secret=${secret}`
const otpauthUrl = `otpauth://totp/${user}?secret=${secret}&issuer=${service}&algorithm=${algorithm}&digits=6&period=30`

return {
secret,
otpauthUrl
}
}

public check(
secret: string,
token: string,
window = 1
): { isValid: boolean; error?: string } {
public check({
secret,
token,
algorithm
}: {
secret: string
token: string
algorithm: 'sha1' | 'sha256' | 'sha512'
}): { isValid: boolean; error?: string } {
const window = 1

const secretRegex = /^[A-Z2-7]+=*$/

if (!secretRegex.test(secret)) {
Expand All @@ -40,7 +57,11 @@ export class Totp {
const currentTime = Math.floor(Date.now() / 1000 / 30)

for (let i = -window; i <= window; i++) {
const generatedToken = this.generateToken(secret, currentTime + i)
const generatedToken = this.generateToken(
secret,
currentTime + i,
algorithm
)

if (!generatedToken) {
return {
Expand Down Expand Up @@ -78,7 +99,11 @@ export class Totp {
return base32
}

private generateToken(secret: string, counter: number): string | null {
private generateToken(
secret: string,
counter: number,
algorithm: 'sha1' | 'sha256' | 'sha512'
): string | null {
const key = this.fromBase32(secret)

if (!key) {
Expand All @@ -93,7 +118,7 @@ export class Totp {
localCounter >>= 8
}

const hmac = crypto.createHmac('sha1', key).update(buffer).digest()
const hmac = crypto.createHmac(algorithm, key).update(buffer).digest()
const offset = hmac[hmac.length - 1] & 0xf
const code =
((hmac[offset] & 0x7f) << 24) |
Expand Down
40 changes: 35 additions & 5 deletions tests/lib/totp/Failure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,39 @@ describe('Time based one tap password Library Failure', () => {
test('should fail with wrong name format', () => {
const totp = new Totp()

const secret = totp.generateSecret({ name: 'Legion API' })
const secret = totp.generateSecret({
user: 'Legion API',
algorithm: 'SHA1',
service: 'MyApp'
})

expect(secret).toStrictEqual({
error: 'Invalid user.'
})
})

test('should fail with wrong service format', () => {
const totp = new Totp()

const secret = totp.generateSecret({
user: 'LegionAPI',
algorithm: 'SHA1',
service: 'My App'
})

expect(secret).toStrictEqual({
error: 'Invalid name.'
error: 'Invalid service.'
})
})

test('should fail with invalid secret base32', () => {
const totp = new Totp()

const secret = totp.check('Invalid Secret', '123545')
const secret = totp.check({
secret: 'Invalid Secret',
algorithm: 'sha1',
token: '456789'
})

expect(secret).toStrictEqual({
isValid: false,
Expand All @@ -26,9 +48,17 @@ describe('Time based one tap password Library Failure', () => {
test('should fail with invalid token', () => {
const totp = new Totp()

const secret = totp.generateSecret({ name: 'LegionAPI' })
const secret = totp.generateSecret({
user: 'Mary',
algorithm: 'SHA1',
service: 'MyApp'
})

const isValid = totp.check(secret.secret ?? '', 'invalid token')
const isValid = totp.check({
algorithm: 'sha1',
secret: secret.secret ?? '',
token: 'Invalid token'
})

expect(isValid).toStrictEqual({
isValid: false,
Expand Down
12 changes: 10 additions & 2 deletions tests/lib/totp/Success.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ describe('Time based one tap password Library', () => {
test('should generate secret and otpauth url and authenticate token successfully', () => {
const totp = new Totp()

const secret = totp.generateSecret({ name: 'LegionAPI' })
const secret = totp.generateSecret({
user: 'Mary',
algorithm: 'SHA1',
service: 'MyApp'
})

expect(secret).toHaveProperty('secret')
expect(secret).toHaveProperty('otpauthUrl')
Expand All @@ -52,7 +56,11 @@ describe('Time based one tap password Library', () => {
const currentTime = Math.floor(Date.now() / 1000 / 30)
const token = generateToken(secret.secret ?? '', currentTime)

const isValid = totp.check(secret.secret ?? '', token)
const isValid = totp.check({
secret: secret.secret ?? '',
algorithm: 'sha1',
token
})
expect(isValid).toStrictEqual({
isValid: true
})
Expand Down

0 comments on commit 5449e8d

Please sign in to comment.