Skip to content

Commit

Permalink
fix: eliminate OTP bias and timing attack vulnerability (#93)
Browse files Browse the repository at this point in the history
* add unbiased random digit generator and timing-safe comparison

* fix(password): use unbiased OTP generation and timing-safe comparison

* fix(code): use unbiased OTP generation and timing-safe comparison

* Create otp-bias-fix.md
  • Loading branch information
sahinfalcon authored Dec 18, 2024
1 parent 70cb2cb commit 8d6a243
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/otp-bias-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openauthjs/openauth": patch
---

fix: eliminate OTP bias and timing attack vulnerability
9 changes: 3 additions & 6 deletions packages/openauth/src/adapter/code.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Context } from "hono"
import { Adapter } from "./adapter.js"
import { generateUnbiasedDigits, timingSafeCompare } from "../random.js"

export type CodeAdapterState =
| {
Expand Down Expand Up @@ -36,11 +37,7 @@ export function CodeAdapter<
}) {
const length = config.length || 6
function generate() {
const buffer = crypto.getRandomValues(new Uint8Array(length))
const otp = Array.from(buffer)
.map((byte) => byte % 10)
.join("")
return otp
return generateUnbiasedDigits(length)
}

return {
Expand Down Expand Up @@ -95,7 +92,7 @@ export function CodeAdapter<
) {
const fd = await c.req.formData()
const compare = fd.get("code")?.toString()
if (!state.code || !compare || state.code !== compare) {
if (!state.code || !compare || !timingSafeCompare(state.code, compare)) {
return transition(
c,
{
Expand Down
11 changes: 4 additions & 7 deletions packages/openauth/src/adapter/password.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UnknownStateError } from "../error.js"
import { Storage } from "../storage/storage.js"
import { Adapter } from "./adapter.js"
import { generateUnbiasedDigits, timingSafeCompare } from "../random.js"

export interface PasswordHasher<T> {
hash(password: string): Promise<T>
Expand Down Expand Up @@ -100,11 +101,7 @@ export type PasswordLoginError =
export function PasswordAdapter(config: PasswordConfig) {
const hasher = config.hasher ?? ScryptHasher()
function generate() {
const buffer = crypto.getRandomValues(new Uint8Array(6))
const otp = Array.from(buffer)
.map((byte) => byte % 10)
.join("")
return otp
return generateUnbiasedDigits(6)
}
return {
type: "password",
Expand Down Expand Up @@ -193,7 +190,7 @@ export function PasswordAdapter(config: PasswordConfig) {

if (action === "verify" && adapter.type === "code") {
const code = fd.get("code")?.toString()
if (!code || code !== adapter.code)
if (!code || !timingSafeCompare(code, adapter.code))
return transition(adapter, { type: "invalid_code" })
const existing = await Storage.get(ctx.storage, [
"email",
Expand Down Expand Up @@ -261,7 +258,7 @@ export function PasswordAdapter(config: PasswordConfig) {

if (action === "verify" && adapter.type === "code") {
const code = fd.get("code")?.toString()
if (!code || code !== adapter.code)
if (!code || !timingSafeCompare(code, adapter.code))
return transition(adapter, { type: "invalid_code" })
return transition({
type: "update",
Expand Down
24 changes: 24 additions & 0 deletions packages/openauth/src/random.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { timingSafeEqual } from "crypto"

export function generateUnbiasedDigits(length: number): string {
const result: number[] = []
while (result.length < length) {
const buffer = crypto.getRandomValues(new Uint8Array(length * 2))
for (const byte of buffer) {
if (byte < 250 && result.length < length) {
result.push(byte % 10)
}
}
}
return result.join("")
}

export function timingSafeCompare(a: string, b: string): boolean {
if (typeof a !== "string" || typeof b !== "string") {
return false
}
if (a.length !== b.length) {
return false
}
return timingSafeEqual(Buffer.from(a), Buffer.from(b))
}

0 comments on commit 8d6a243

Please sign in to comment.