rewrite without bun, using Crypto API
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export * as pwd from './src/pwd'
|
||||
import {hash, verify} from './src/pwd'
|
||||
export const pwd = {hash, verify}
|
||||
|
||||
+11
-15
@@ -1,23 +1,19 @@
|
||||
{
|
||||
"name": "crypto-server",
|
||||
"description": "Various crypto utils that only works on the server side",
|
||||
"description": "Various crypto utils that only work on the server side",
|
||||
"version": "1.0.0",
|
||||
|
||||
"author": "Pascal Perrenoud <pascal@pband.ch>",
|
||||
|
||||
"dependencies": {
|
||||
"log": "git+https://git.pband.ch/typescript/log.git",
|
||||
"misc": "git+https://git.pband.ch/typescript/misc.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.6",
|
||||
"logger-console": "git+https://git.pband.ch/typescript/logger-console.git"
|
||||
},
|
||||
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"files": ["index.ts", "src"],
|
||||
|
||||
"scripts": {
|
||||
"test": "bun test"
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"jose": "^5.3.0",
|
||||
"log": "git+https://git.pband.ch/typescript/log.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.2"
|
||||
}
|
||||
"files": ["index.ts", "src"]
|
||||
}
|
||||
|
||||
+136
-10
@@ -1,16 +1,142 @@
|
||||
import logger from 'log'
|
||||
import * as misc from 'misc'
|
||||
|
||||
const log = logger('crypto-server:pwd')
|
||||
|
||||
export function hash(pwd: string): Promise<string> {
|
||||
log.debug('hash password')
|
||||
return Bun.password.hash(pwd)
|
||||
type Salt = Uint8Array
|
||||
type Hash = ArrayBuffer
|
||||
type Iterations = number
|
||||
|
||||
export enum Strength {
|
||||
Easy = 100_000,
|
||||
Medium = 300_000,
|
||||
Hard = 750_000,
|
||||
}
|
||||
|
||||
export async function verify(pwd: string, hash: string): Promise<boolean> {
|
||||
log.debug("verify password's hash")
|
||||
return Bun.password.verify(pwd, hash).catch(e => {
|
||||
log.warn('Password verification failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
return false
|
||||
})
|
||||
export async function hash(password: string, strength: Strength = Strength.Medium) : Promise<string> {
|
||||
log.debug('hash password')
|
||||
const iterations = strength as number
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16))
|
||||
const h = await internal_hash(password, iterations, salt)
|
||||
return encode(salt, iterations, h)
|
||||
}
|
||||
export async function verify(password: string, hash: string) : Promise<boolean> {
|
||||
log.debug("verify password's hash")
|
||||
const params = decode(hash)
|
||||
if (params === null) {
|
||||
log.warn('Invalid hash')
|
||||
return false
|
||||
}
|
||||
const h = await internal_hash(password, params[1], params[0])
|
||||
return compare(h, params[2])
|
||||
}
|
||||
|
||||
export function encode(salt: Salt, iterations: Iterations, hash: Hash) : string {
|
||||
const s = salt_encode(salt)
|
||||
const i = iterations_encode(iterations)
|
||||
const h = hash_encode(hash)
|
||||
|
||||
// version.iterations$salt.hash
|
||||
return `1.${i}$${s}.${h}`
|
||||
}
|
||||
export function decode(hash: string) : [Salt, Iterations, Hash] | null {
|
||||
// TODO : Log
|
||||
const parts = hash.split('$')
|
||||
if (parts.length !== 2) return null
|
||||
|
||||
const p1 = parts[0].split('.')
|
||||
if (p1.length !== 2) return null
|
||||
|
||||
const p2 = parts[1].split('.')
|
||||
if (p2.length !== 2) return null
|
||||
|
||||
// Version
|
||||
if (p1[0] !== '1') return null
|
||||
|
||||
const iterations = iterations_decode(p1[1])
|
||||
if (iterations === null) return null
|
||||
|
||||
const salt = salt_decode(p2[0])
|
||||
if (salt === null) return null
|
||||
|
||||
const h = hash_decode(p2[1])
|
||||
if (h === null) return null
|
||||
|
||||
return [salt, iterations, h]
|
||||
}
|
||||
|
||||
export function iterations_encode(iterations: Iterations) : string {
|
||||
return iterations.toString(16)
|
||||
}
|
||||
export function iterations_decode(iterations: string) : Iterations | null {
|
||||
try {
|
||||
return parseInt(iterations, 16)
|
||||
} catch(e) {
|
||||
log.warn('Invalid iterations count')
|
||||
log.debug('Error', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function salt_encode(salt: Salt) : string {
|
||||
return misc.a2b64(salt)
|
||||
}
|
||||
export function salt_decode(salt: string) : Salt | null {
|
||||
const res = misc.b642a(salt)
|
||||
if (res.is_err()) {
|
||||
log.warn('Invalid salt')
|
||||
return null
|
||||
} else {
|
||||
return res.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
export function hash_encode(hash: Hash) : string {
|
||||
return misc.a2b64(new Uint8Array(hash))
|
||||
}
|
||||
export function hash_decode(hash: string) : Hash | null {
|
||||
const res = misc.b642a(hash)
|
||||
if (res.is_err()) {
|
||||
log.warn('Invalid hash')
|
||||
return null
|
||||
} else {
|
||||
return res.unwrap().buffer as ArrayBuffer
|
||||
}
|
||||
}
|
||||
|
||||
export async function internal_hash(password: string, iterations: Iterations, salt: Salt): Promise<Hash> {
|
||||
const pwd = await encode_password(password)
|
||||
return crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
hash: 'SHA-512',
|
||||
iterations,
|
||||
salt,
|
||||
},
|
||||
pwd,
|
||||
256
|
||||
)
|
||||
}
|
||||
export function compare(a: Hash, b: Hash) : boolean {
|
||||
const a1 = new Uint8Array(a)
|
||||
const b1 = new Uint8Array(b)
|
||||
|
||||
let result = a1.length === b1.length
|
||||
for (let i = 0;i < Math.max(a1.length, b1.length); ++i) {
|
||||
const a2 = a1[i] ?? 'a'
|
||||
const b2 = b1[i] ?? 'b'
|
||||
const test = a2 === b2
|
||||
result = result && test
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
export async function encode_password(password: string) : Promise<CryptoKey> {
|
||||
return crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits']
|
||||
)
|
||||
}
|
||||
|
||||
+56
-8
@@ -1,14 +1,25 @@
|
||||
import {test, expect} from 'bun:test'
|
||||
import {beforeAll, describe, test, expect} from 'bun:test'
|
||||
import {Level, options, writers} from 'log'
|
||||
import {Console} from 'logger-console'
|
||||
|
||||
import {pwd} from '../index'
|
||||
import * as pwd from '../src/pwd'
|
||||
|
||||
const console = new Console({
|
||||
minLevel: Level.DEBUG,
|
||||
with_color: true,
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
options.verbose = false
|
||||
writers.set('console', console)
|
||||
})
|
||||
|
||||
test('base case', async () => {
|
||||
const password = "AwesomePassword123!"
|
||||
const hash = await pwd.hash(password)
|
||||
const verification = await pwd.verify(password, hash)
|
||||
expect(verification).toBe(true)
|
||||
expect(verification).toBeTrue()
|
||||
})
|
||||
|
||||
test('wrong password', async () => {
|
||||
const password1 = "AwesomePassword123!"
|
||||
const password2 = "AwesomePassword321!"
|
||||
@@ -16,20 +27,57 @@ test('wrong password', async () => {
|
||||
|
||||
const hash = await pwd.hash(password1)
|
||||
const verification = await pwd.verify(password2, hash)
|
||||
expect(verification).toBe(false)
|
||||
expect(verification).toBeFalse()
|
||||
})
|
||||
test("Empty password isn't a trick", async () => {
|
||||
const p1 = ""
|
||||
const p2 = "abc"
|
||||
|
||||
const hash = await pwd.hash(p1)
|
||||
const verification = await pwd.verify(p2, hash)
|
||||
expect(verification).toBeFalse()
|
||||
})
|
||||
test('salt changes', async () => {
|
||||
const password = "AwesomePassword123!"
|
||||
const hash1 = await pwd.hash(password)
|
||||
const hash2 = await pwd.hash(password)
|
||||
expect(hash1).not.toEqual(hash2)
|
||||
})
|
||||
|
||||
test('tampered hash', async () => {
|
||||
const password = "AwesomePassword123"
|
||||
const hash = await pwd.hash(password)
|
||||
const tamperedHash = hash.replace('a', 'b')
|
||||
|
||||
const tamperedHash = hash
|
||||
.replace('a', 'b')
|
||||
.replace('c', 'd')
|
||||
.replace('e', 'f')
|
||||
expect(tamperedHash).not.toEqual(hash)
|
||||
|
||||
const verification = await pwd.verify(password, tamperedHash)
|
||||
expect(verification).toBe(false)
|
||||
expect(verification).toBeFalse()
|
||||
})
|
||||
|
||||
describe('Serialization', () => {
|
||||
describe('Iterations', () => {
|
||||
test('base case', () => {
|
||||
const i = 10000
|
||||
const ser = pwd.iterations_encode(i)
|
||||
const de = pwd.iterations_decode(ser)
|
||||
expect(de).toEqual(i)
|
||||
})
|
||||
})
|
||||
describe('Hash', () => {
|
||||
test('base case', () => {
|
||||
const h = new Uint8Array(16).buffer as ArrayBuffer
|
||||
const ser = pwd.hash_encode(h)
|
||||
const de = pwd.hash_decode(ser)
|
||||
expect(de).toEqual(h)
|
||||
})
|
||||
})
|
||||
describe('Salt', () => {
|
||||
const salt = new Uint8Array(16)
|
||||
const ser = pwd.salt_encode(salt)
|
||||
const de = pwd.salt_decode(ser)
|
||||
expect(de).toBe(salt)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user