-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c69d935
commit d24cfc4
Showing
11 changed files
with
313 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { generateHotp, verifyHotp } from "./hotp.ts"; | ||
|
||
const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; | ||
|
||
Deno.bench("generateHotp()", async () => { | ||
await generateHotp(secret, 1000000000), "270103"; | ||
}); | ||
|
||
Deno.bench("verifyHotp()", async () => { | ||
await verifyHotp("270103", secret, 1000000000); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { assert, assertEquals } from "@std/assert"; | ||
import { | ||
counterToBuffer, | ||
generateHmacSha1, | ||
generateHotp, | ||
truncate, | ||
verifyHotp, | ||
} from "./hotp.ts"; | ||
import { decodeBase32, encodeHex } from "@std/encoding"; | ||
|
||
const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; | ||
const secretBytes = decodeBase32(secret); | ||
|
||
Deno.test("counterToBuffer()", () => { | ||
assertEquals( | ||
new Uint8Array(counterToBuffer(0).buffer), | ||
new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]), | ||
); | ||
assertEquals( | ||
new Uint8Array(counterToBuffer(100).buffer), | ||
new Uint8Array([0, 0, 0, 0, 0, 0, 0, 100]), | ||
); | ||
assertEquals( | ||
new Uint8Array(counterToBuffer(1000).buffer), | ||
new Uint8Array([0, 0, 0, 0, 0, 0, 3, 232]), | ||
); | ||
assertEquals( | ||
new Uint8Array(counterToBuffer(1000000000000).buffer), | ||
new Uint8Array([0, 0, 0, 232, 212, 165, 16, 0]), | ||
); | ||
}); | ||
|
||
Deno.test("generateHmacSha1() should generate hashes", async () => { | ||
const data = new Uint8Array([0, 0, 0, 0, 0, 0, 3, 232]); | ||
const hmac = await generateHmacSha1(secretBytes, data); | ||
assertEquals(encodeHex(hmac), "a5a0ee362d45dcb90fba5efb57ac90c2903b2c59"); | ||
}); | ||
|
||
Deno.test("truncate() should output a numbered string", () => { | ||
const data = new Uint8Array([0, 0, 0, 232, 212, 165, 16, 0]); | ||
const numberedString0 = truncate(data, 0); | ||
assertEquals(numberedString0, "0"); | ||
const numberedString1 = truncate(data, 1); | ||
assertEquals(numberedString1, "2"); | ||
const numberedString6 = truncate(data, 6); | ||
assertEquals(numberedString6, "000232"); | ||
const numberedString10 = truncate(data, 10); | ||
assertEquals(numberedString10, "0000000232"); | ||
}); | ||
|
||
Deno.test("generateHotp()", async () => { | ||
assertEquals(await generateHotp(secret, 0), "187492"); | ||
assertEquals(await generateHotp(secret, 100), "907306"); | ||
assertEquals(await generateHotp(secret, 1000), "303255"); | ||
assertEquals(await generateHotp(secret, 1000000000000), "270103"); | ||
}); | ||
|
||
Deno.test("verifyHotp()", async () => { | ||
assert(await verifyHotp("187492", secret, 0)); | ||
assert(await verifyHotp("907306", secret, 100)); | ||
assert(await verifyHotp("303255", secret, 1000)); | ||
assert(await verifyHotp("270103", secret, 1000000000000)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { decodeBase32 } from "@std/encoding"; | ||
|
||
/** Converts a counter value to a DataView. | ||
* | ||
* @ignore | ||
*/ | ||
export function counterToBuffer(counter: number): DataView { | ||
const buffer = new DataView(new ArrayBuffer(8)); | ||
buffer.setBigUint64(0, BigInt(counter), false); | ||
return buffer; | ||
} | ||
|
||
/** Generates a HMAC-SHA1 hash of the specified key and counter. | ||
* | ||
* @ignore | ||
*/ | ||
export async function generateHmacSha1( | ||
key: Uint8Array, | ||
data: BufferSource, | ||
): Promise<Uint8Array> { | ||
const importedKey = await crypto.subtle.importKey( | ||
"raw", | ||
key, | ||
{ name: "HMAC", hash: "SHA-1" }, | ||
false, | ||
["sign"], | ||
); | ||
|
||
const signedData = await crypto.subtle.sign( | ||
"HMAC", | ||
importedKey, | ||
data, | ||
); | ||
|
||
return new Uint8Array(signedData); | ||
} | ||
|
||
/** Truncates the HMAC value to a 6-digit HOTP value. | ||
* | ||
* @ignore | ||
*/ | ||
export function truncate(value: Uint8Array, length: number): string { | ||
const offset = value[19] & 0xf; | ||
const code = (value[offset] & 0x7f) << 24 | | ||
(value[offset + 1] & 0xff) << 16 | | ||
(value[offset + 2] & 0xff) << 8 | | ||
(value[offset + 3] & 0xff); | ||
const digits = code % Math.pow(10, length); | ||
return digits.toString().padStart(length, "0"); | ||
} | ||
|
||
/** Generates a HMAC-based one-time password (HOTP) using the specified key and counter. | ||
* | ||
* @param key a secret key used to generate the HOTP. Can be a string in base32 encoding or a Uint8Array. | ||
* @param counter a counter value used to generate the HOTP. | ||
* @returns a 6-digit HOTP value. | ||
*/ | ||
export async function generateHotp( | ||
key: string | Uint8Array, | ||
counter: number, | ||
): Promise<string> { | ||
const parsedKey = typeof key === "string" ? decodeBase32(key) : key; | ||
const buffer = counterToBuffer(counter); | ||
|
||
const hmac = await generateHmacSha1(parsedKey, buffer); | ||
return truncate(hmac, 6); | ||
} | ||
|
||
/** Verifies a HMAC-based one-time password (HOTP) using the specified key and counter. | ||
* | ||
* @param otp the one-time password to verify. | ||
* @param key a secret key used to generate the HOTP. Can be a string in base32 encoding or a Uint8Array. | ||
* @param counter a counter value used to generate the HOTP. | ||
*/ | ||
export async function verifyHotp( | ||
otp: string, | ||
key: string | Uint8Array, | ||
counter: number, | ||
): Promise<boolean> { | ||
return otp === await generateHotp(key, counter); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { generateTotp, verifyTotp } from "./totp.ts"; | ||
|
||
const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; | ||
|
||
Deno.bench("generateTotp()", async () => { | ||
await generateTotp(secret, 1000000000), "270103"; | ||
}); | ||
|
||
Deno.bench("verifyTotp()", async () => { | ||
await verifyTotp("270103", secret, 1000000000); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { assert, assertEquals } from "@std/assert"; | ||
import { generateTotp, verifyTotp } from "./totp.ts"; | ||
|
||
const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; | ||
const t = 1704067200000; | ||
|
||
Deno.test("generateTotp()", async () => { | ||
console.log(); | ||
assertEquals(await generateTotp(secret, 0, t), "342743"); | ||
assertEquals(await generateTotp(secret, 944996400, t), "149729"); | ||
assertEquals(await generateTotp(secret, 976618800, t), "372018"); | ||
assertEquals(await generateTotp(secret, 1723245550, t), "665341"); | ||
assertEquals(await generateTotp(secret, t, t), "187492"); | ||
}); | ||
|
||
Deno.test("verifyTotp()", async () => { | ||
assert(await verifyTotp("342743", secret, 0, t)); | ||
assert(await verifyTotp("149729", secret, 944996400, t)); | ||
assert(await verifyTotp("372018", secret, 976618800, t)); | ||
assert(await verifyTotp("665341", secret, 1723245550, t)); | ||
assert(await verifyTotp("187492", secret, t, t)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { generateHotp } from "./hotp.ts"; | ||
|
||
/** Generate a TOTP value from a key and a time. | ||
* | ||
* @param key a secret key used to generate the HOTP. Can be a string in base32 encoding or a Uint8Array. | ||
* @param t0 the initial time to use for the counter. | ||
* @returns the TOTP value. | ||
*/ | ||
export function generateTotp( | ||
key: string | Uint8Array, | ||
t0 = 0, | ||
t = Date.now(), | ||
): Promise<string> { | ||
const counter = Math.floor((t - t0) / 30000); | ||
return generateHotp(key, counter); | ||
} | ||
|
||
/** Verifies a TOTP value from a key and a time. | ||
* | ||
* @param otp the one-time password to verify. | ||
* @param key a secret key used to generate the HOTP. Can be a string in base32 encoding or a Uint8Array. | ||
* @param t0 the initial time to use for the counter. | ||
*/ | ||
export async function verifyTotp( | ||
otp: string, | ||
key: string | Uint8Array, | ||
t0 = 0, | ||
t = Date.now(), | ||
): Promise<boolean> { | ||
return otp === await generateTotp(key, t0, t); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { assertEquals } from "@std/assert"; | ||
import { generateSecret, generateSecretBytes } from "./utils.ts"; | ||
|
||
for (const l of [1, 8, 20, 100, 1000]) { | ||
Deno.bench(`generateSecretBytes - ${l}`, () => { | ||
const secretBytes = generateSecretBytes(l); | ||
assertEquals(secretBytes.length, l); | ||
}); | ||
|
||
Deno.bench(`generateSecret - ${l}`, () => { | ||
const secret = generateSecret(l); | ||
assertEquals(secret.length, l); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { assertEquals } from "@std/assert"; | ||
import { generateSecret, generateSecretBytes } from "./utils.ts"; | ||
|
||
Deno.test("generateSecretBytes - default", () => { | ||
const secretBytes = generateSecretBytes(); | ||
assertEquals(secretBytes.length, 20); | ||
}); | ||
|
||
Deno.test("generateSecret - default", () => { | ||
const secret = generateSecret(); | ||
assertEquals(secret.length, 20); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { encodeBase32 } from "@std/encoding"; | ||
|
||
/** Generates a secret key of the specified length. | ||
* | ||
* @param length how many bytes the secret key should be. | ||
* @returns a secret as a byte array | ||
*/ | ||
export function generateSecretBytes(length: number = 20): Uint8Array { | ||
const buffer = new Uint8Array(length); | ||
crypto.getRandomValues(buffer); | ||
return buffer; | ||
} | ||
|
||
/** Generates a secret key of the specified length in selected encoding | ||
* | ||
* @param length how many characters the secret key should be. | ||
* @returns a secret string | ||
*/ | ||
export function generateSecret( | ||
length: number = 20, | ||
): string { | ||
const buffer = generateSecretBytes(Math.max(6, length)); | ||
|
||
const encoded = encodeBase32(buffer); | ||
|
||
return encoded.replaceAll("=", "").slice(-length); | ||
} |