This commit is contained in:
Pascal Perrenoud
2024-07-27 13:35:17 +02:00
parent 5cc375bd5a
commit 81ec3e9ed0
7 changed files with 364 additions and 390 deletions
+50 -46
View File
@@ -6,53 +6,57 @@ const log = logger('crypto:jwt')
export type Key = jose.KeyLike | Uint8Array
export class JWTcontext {
private constructor(
private readonly key: Key,
) {}
private constructor(private readonly key: Key) {}
public static async gen_key(): Promise<Key> {
log.trace("generate key")
return jose.generateSecret("HS512")
}
public static new(key: Key) : JWTcontext {
return new JWTcontext(key)
}
public static async new_random() : Promise<JWTcontext> {
const k = await JWTcontext.gen_key()
return new JWTcontext(k)
public static async gen_key(): Promise<Key> {
log.trace('generate key')
return jose.generateSecret('HS512')
}
public static new(key: Key): JWTcontext {
return new JWTcontext(key)
}
public static async new_random(): Promise<JWTcontext> {
const k = await JWTcontext.gen_key()
return new JWTcontext(k)
}
public async sign<T>(
message: T,
set_issued: boolean = false,
exp?: number | string | Date,
audience?: string | string[],
issuer?: string
): Promise<string> {
log.trace('sign JWT')
log.trace('Config :', {
set_issued,
exp,
issuer
})
let jwt = new jose.SignJWT({message}).setProtectedHeader({alg: 'HS512'})
if (set_issued) jwt = jwt.setIssuedAt()
if (issuer !== undefined) jwt = jwt.setIssuer(issuer)
if (audience !== undefined) jwt = jwt.setAudience(audience)
if (exp !== undefined) jwt = jwt.setExpirationTime(exp)
return await jwt.sign(this.key)
}
public async verify<T>(jwt: string, audience?: string | string[], issuer?: string | string[]): Promise<T | null> {
log.debug('Verify JWT')
log.trace('Issuer :', issuer)
log.trace('Audience :', audience)
try {
let payload = await jose.jwtVerify(jwt, this.key, {audience, issuer})
return payload.payload.message as T
} catch (e) {
log.warn('JWT verification failed')
log.debug(`Error : ${e}`)
}
public async sign<T>(message: T, set_issued: boolean = false, exp?: number | string | Date, audience?: string | string[], issuer?: string): Promise<string> {
log.trace('sign JWT')
log.trace('Config :', {
set_issued,
exp,
issuer,
})
let jwt = new jose.SignJWT({message}).setProtectedHeader({alg: "HS512"})
if (set_issued) jwt = jwt.setIssuedAt()
if (issuer !== undefined) jwt = jwt.setIssuer(issuer)
if (audience !== undefined) jwt = jwt.setAudience(audience)
if (exp !== undefined) jwt = jwt.setExpirationTime(exp)
return await jwt.sign(this.key)
}
public async verify<T>(jwt: string, audience?: string | string[], issuer?: string | string[]): Promise<T | null> {
log.debug('Verify JWT')
log.trace('Issuer :', issuer)
log.trace('Audience :', audience)
try {
let payload = await jose.jwtVerify(jwt, this.key, {audience, issuer})
return payload.payload.message as T
} catch (e) {
log.warn('JWT verification failed')
log.debug(`Error : ${e}`)
}
return null
}
return null
}
}
+70 -80
View File
@@ -2,89 +2,79 @@ import logger from 'log'
const log = logger('crypto:kdf')
export enum Usage {
sign,
wrap,
sign,
wrap
}
export async function hkdf(key: Uint8Array, usage: Usage) : Promise<CryptoKey | Uint8Array> {
log.trace("HKDF")
log.trace(`usage : ${usage === Usage.sign ? 'sign' : 'wrap'}`)
export async function hkdf(key: Uint8Array, usage: Usage): Promise<CryptoKey | Uint8Array> {
log.trace('HKDF')
log.trace(`usage : ${usage === Usage.sign ? 'sign' : 'wrap'}`)
const material = await crypto.subtle.importKey(
"raw",
key,
"HKDF",
false,
["deriveKey", "deriveBits"],
)
const material = await crypto.subtle.importKey('raw', key, 'HKDF', false, ['deriveKey', 'deriveBits'])
if (usage === Usage.wrap) {
return crypto.subtle.deriveKey(
{
name: "HKDF",
hash: "SHA-512",
salt: new Uint8Array(32),
info: new TextEncoder().encode("wrap"),
},
material,
{name: "AES-GCM", length: 256},
false,
["wrapKey", "unwrapKey"],
)
} else if (usage === Usage.sign) {
const buffer = await crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-512",
salt: new Uint8Array(32),
info: new TextEncoder().encode("sign"),
},
material,
512
)
return new Uint8Array(buffer)
} else {
log.warn(`Called HKDF with unknown enum value : ${usage}`)
throw "I don't even know what to say."
}
}
export async function pbkdf(salt: Uint8Array, password: string) : Promise<Uint8Array> {
log.trace("PBKDF")
const material = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveBits"],
)
const buffer = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt,
iterations: 250_000,
hash: "SHA-512",
},
material,
256,
)
return new Uint8Array(buffer)
}
export function ecdh(privkey: CryptoKey, pubkey: CryptoKey) : Promise<CryptoKey> {
log.trace("ECDH")
if (usage === Usage.wrap) {
return crypto.subtle.deriveKey(
{
name: "ECDH",
public: pubkey,
},
privkey,
{
name: "AES-GCM",
length: 256,
},
false,
["wrapKey", "unwrapKey"],
{
name: 'HKDF',
hash: 'SHA-512',
salt: new Uint8Array(32),
info: new TextEncoder().encode('wrap')
},
material,
{name: 'AES-GCM', length: 256},
false,
['wrapKey', 'unwrapKey']
)
} else if (usage === Usage.sign) {
const buffer = await crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-512',
salt: new Uint8Array(32),
info: new TextEncoder().encode('sign')
},
material,
512
)
return new Uint8Array(buffer)
} else {
log.warn(`Called HKDF with unknown enum value : ${usage}`)
throw "I don't even know what to say."
}
}
export async function pbkdf(salt: Uint8Array, password: string): Promise<Uint8Array> {
log.trace('PBKDF')
const material = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, [
'deriveBits'
])
const buffer = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt,
iterations: 250_000,
hash: 'SHA-512'
},
material,
256
)
return new Uint8Array(buffer)
}
export function ecdh(privkey: CryptoKey, pubkey: CryptoKey): Promise<CryptoKey> {
log.trace('ECDH')
return crypto.subtle.deriveKey(
{
name: 'ECDH',
public: pubkey
},
privkey,
{
name: 'AES-GCM',
length: 256
},
false,
['wrapKey', 'unwrapKey']
)
}
+34 -47
View File
@@ -7,57 +7,44 @@ import {ecdh} from './kdf'
const log = logger('crypto:private-wrap')
const algorithm = {
name: "ECDH",
namedCurve: "P-521",
name: 'ECDH',
namedCurve: 'P-521'
}
export class PrivateWrap {
private constructor(
private readonly box: SecretWrap,
private readonly pubkey: CryptoKey,
) {}
private constructor(private readonly box: SecretWrap, private readonly pubkey: CryptoKey) {}
public static gen_keypair(extractable : boolean = true) : Promise<CryptoKeyPair> {
log.trace("generate keypair")
return crypto.subtle.generateKey(
algorithm,
extractable,
["deriveKey"],
)
}
public static gen_keypair(extractable: boolean = true): Promise<CryptoKeyPair> {
log.trace('generate keypair')
return crypto.subtle.generateKey(algorithm, extractable, ['deriveKey'])
}
public static async encrypt(data: CryptoKey, pubkey: CryptoKey) : Promise<PrivateWrap> {
log.trace("encrypt")
const k = await this.gen_keypair()
const kd = await ecdh(k.privateKey, pubkey)
const box = await SecretWrap.encrypt(data, kd)
return new this(box, k.publicKey)
}
public async decrypt(privkey: CryptoKey) : Promise<CryptoKey> {
log.trace("decrypt")
const kd = await ecdh(privkey, this.pubkey)
return this.box.decrypt(kd)
}
public static async encrypt(data: CryptoKey, pubkey: CryptoKey): Promise<PrivateWrap> {
log.trace('encrypt')
const k = await this.gen_keypair()
const kd = await ecdh(k.privateKey, pubkey)
const box = await SecretWrap.encrypt(data, kd)
return new this(box, k.publicKey)
}
public async decrypt(privkey: CryptoKey): Promise<CryptoKey> {
log.trace('decrypt')
const kd = await ecdh(privkey, this.pubkey)
return this.box.decrypt(kd)
}
public async toString(): Promise<string> {
log.trace("toString")
const pubkey_spki = await crypto.subtle.exportKey("spki", this.pubkey)
const pubkey = a2b64(new Uint8Array(pubkey_spki))
const box = this.box.toString()
return `${pubkey}.${box}`
}
public static async fromString(data: string): Promise<PrivateWrap> {
log.trace("fromString")
const parts = data.split(".", 2)
const pubkey_str = b642a(parts[0]).expect("Failed to decode pubkey")
const pubkey = await crypto.subtle.importKey(
"spki",
pubkey_str,
algorithm,
true,
[],
)
const box = SecretWrap.fromString(parts[1])
return new PrivateWrap(box, pubkey)
}
public async toString(): Promise<string> {
log.trace('toString')
const pubkey_spki = await crypto.subtle.exportKey('spki', this.pubkey)
const pubkey = a2b64(new Uint8Array(pubkey_spki))
const box = this.box.toString()
return `${pubkey}.${box}`
}
public static async fromString(data: string): Promise<PrivateWrap> {
log.trace('fromString')
const parts = data.split('.', 2)
const pubkey_str = b642a(parts[0]).expect('Failed to decode pubkey')
const pubkey = await crypto.subtle.importKey('spki', pubkey_str, algorithm, true, [])
const box = SecretWrap.fromString(parts[1])
return new PrivateWrap(box, pubkey)
}
}
+50 -53
View File
@@ -4,60 +4,57 @@ import {a2b64, b642a} from 'misc'
const log = logger('crypto:secret-box')
export class SecretBox {
private constructor(
private readonly iv: Uint8Array,
private readonly cipher: Uint8Array,
) {}
private constructor(private readonly iv: Uint8Array, private readonly cipher: Uint8Array) {}
public static gen_key(extractable : boolean = true) : Promise<CryptoKey> {
log.trace("generate key")
return crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
extractable,
["encrypt", "decrypt"],
)
}
public static gen_key(extractable: boolean = true): Promise<CryptoKey> {
log.trace('generate key')
return crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
extractable,
['encrypt', 'decrypt']
)
}
public static async encrypt(data: Uint8Array, key: CryptoKey) : Promise<SecretBox> {
log.trace("encrypt")
const iv = crypto.getRandomValues(new Uint8Array(11))
const cipher = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
},
key,
data
)
return new SecretBox(iv, new Uint8Array(cipher))
}
public async decrypt(key: CryptoKey) : Promise<Uint8Array> {
log.trace("decrypt")
const buffer = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: this.iv
},
key,
this.cipher
)
return new Uint8Array(buffer)
}
public static async encrypt(data: Uint8Array, key: CryptoKey): Promise<SecretBox> {
log.trace('encrypt')
const iv = crypto.getRandomValues(new Uint8Array(11))
const cipher = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
key,
data
)
return new SecretBox(iv, new Uint8Array(cipher))
}
public async decrypt(key: CryptoKey): Promise<Uint8Array> {
log.trace('decrypt')
const buffer = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: this.iv
},
key,
this.cipher
)
return new Uint8Array(buffer)
}
public toString() : string {
log.trace("toString")
const iv = a2b64(this.iv)
const cipher = a2b64(this.iv)
return `${iv}.${cipher}`
}
public static fromString(data: string) : SecretBox {
log.trace("fromString")
const parts = data.split(".", 2)
const iv = b642a(parts[0]).expect("Failed decode IV")
const cipher = b642a(parts[1]).expect("Failed to decode cipher")
return new SecretBox(iv, cipher)
}
public toString(): string {
log.trace('toString')
const iv = a2b64(this.iv)
const cipher = a2b64(this.iv)
return `${iv}.${cipher}`
}
public static fromString(data: string): SecretBox {
log.trace('fromString')
const parts = data.split('.', 2)
const iv = b642a(parts[0]).expect('Failed decode IV')
const cipher = b642a(parts[1]).expect('Failed to decode cipher')
return new SecretBox(iv, cipher)
}
}
+69 -71
View File
@@ -4,80 +4,78 @@ import {a2b64, b642a} from 'misc'
const log = logger('crypto:secret-wrap')
export class SecretWrap {
constructor(
private readonly cipher: Uint8Array,
private readonly algorithm: KeyAlgorithm,
private readonly usages: KeyUsage[],
private readonly type: "raw" | "pkcs8",
private readonly iv: Uint8Array,
) {}
constructor(
private readonly cipher: Uint8Array,
private readonly algorithm: KeyAlgorithm,
private readonly usages: KeyUsage[],
private readonly type: 'raw' | 'pkcs8',
private readonly iv: Uint8Array
) {}
public static gen_key(extractable : boolean = true) : Promise<CryptoKey> {
log.trace("generate key")
return crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
extractable,
["wrapKey", "unwrapKey"],
)
}
public static gen_key(extractable: boolean = true): Promise<CryptoKey> {
log.trace('generate key')
return crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
extractable,
['wrapKey', 'unwrapKey']
)
}
private static format(type: KeyType) : "raw" | "pkcs8" {
switch (type) {
case "private": return "pkcs8"
case "secret": return "raw"
default: throw "Don't wrap public keys please..."
}
private static format(type: KeyType): 'raw' | 'pkcs8' {
switch (type) {
case 'private':
return 'pkcs8'
case 'secret':
return 'raw'
default:
throw "Don't wrap public keys please..."
}
}
public static async encrypt(data: CryptoKey, key: CryptoKey) : Promise<SecretWrap> {
log.trace("encrypt")
const format = this.format(data.type)
const iv = crypto.getRandomValues(new Uint8Array(12))
const box = await crypto.subtle.wrapKey(
format,
data,
key,
{
name: "AES-GCM",
iv,
},
)
return new SecretWrap(new Uint8Array(box), data.algorithm, data.usages, format, iv)
}
public async decrypt(key: CryptoKey) : Promise<CryptoKey> {
log.trace("decrypt")
return crypto.subtle.unwrapKey(
this.type,
this.cipher,
key,
{
name: "AES-GCM",
iv: this.iv,
},
this.algorithm,
true,
this.usages
)
}
public static async encrypt(data: CryptoKey, key: CryptoKey): Promise<SecretWrap> {
log.trace('encrypt')
const format = this.format(data.type)
const iv = crypto.getRandomValues(new Uint8Array(12))
const box = await crypto.subtle.wrapKey(format, data, key, {
name: 'AES-GCM',
iv
})
return new SecretWrap(new Uint8Array(box), data.algorithm, data.usages, format, iv)
}
public async decrypt(key: CryptoKey): Promise<CryptoKey> {
log.trace('decrypt')
return crypto.subtle.unwrapKey(
this.type,
this.cipher,
key,
{
name: 'AES-GCM',
iv: this.iv
},
this.algorithm,
true,
this.usages
)
}
public toString() : string {
log.trace("toString")
return JSON.stringify({
cipher: a2b64(this.cipher),
iv: a2b64(this.iv),
algorithm: this.algorithm,
usages: this.usages,
type: this.type,
})
}
public static fromString(data: string) : SecretWrap {
log.trace("fromString")
const obj = JSON.parse(data)
const cipher = b642a(obj.cipher).expect("Failed to decode cipher")
const iv = b642a(obj.iv).expect("Failed to decode IV")
return new SecretWrap(cipher, obj.algorithm, obj.usages, obj.type, iv)
}
public toString(): string {
log.trace('toString')
return JSON.stringify({
cipher: a2b64(this.cipher),
iv: a2b64(this.iv),
algorithm: this.algorithm,
usages: this.usages,
type: this.type
})
}
public static fromString(data: string): SecretWrap {
log.trace('fromString')
const obj = JSON.parse(data)
const cipher = b642a(obj.cipher).expect('Failed to decode cipher')
const iv = b642a(obj.iv).expect('Failed to decode IV')
return new SecretWrap(cipher, obj.algorithm, obj.usages, obj.type, iv)
}
}
+60 -62
View File
@@ -9,74 +9,72 @@ import type {ProjPointType} from '@noble/curves/abstract/weierstrass'
* @param source entropy source, 64 bytes
* @returns [PrivKey, PubKey]
*/
export async function derive_keypair(source: Uint8Array) : Promise<[CryptoKey, CryptoKey]> {
const derived = hkdf(sha512, source, undefined, 'sign', 512)
const validPrivateKey = mod.mapHashToField(derived, p521.CURVE.n) // Dodge modulus bias :) (kinda)
const d = a2bg(validPrivateKey)
const point = get_pubkey(d)
export async function derive_keypair(source: Uint8Array): Promise<[CryptoKey, CryptoKey]> {
const derived = hkdf(sha512, source, undefined, 'sign', 512)
const validPrivateKey = mod.mapHashToField(derived, p521.CURVE.n) // Dodge modulus bias :) (kinda)
const d = a2bg(validPrivateKey)
const point = get_pubkey(d)
const pubkey = await crypto_pubkey(point)
const privkey = await crypto_privkey(d, point)
const pubkey = await crypto_pubkey(point)
const privkey = await crypto_privkey(d, point)
return [privkey, pubkey]
return [privkey, pubkey]
}
function get_pubkey(d: bigint) : ProjPointType<bigint> {
return p521.ProjectivePoint.BASE.multiply(d)
function get_pubkey(d: bigint): ProjPointType<bigint> {
return p521.ProjectivePoint.BASE.multiply(d)
}
function crypto_privkey(d: bigint, point: ProjPointType<bigint>) : Promise<CryptoKey> {
const jwk = {
crv: "P-521",
d: bg2b64url(d),
ext: false,
key_ops: [ "sign" ],
kty: "EC",
x: bg2b64url(point.x),
y: bg2b64url(point.y),
}
return crypto.subtle.importKey(
"jwk",
jwk,
{
name: "ECDSA",
namedCurve: "P-521"
},
false,
["sign"]
)
function crypto_privkey(d: bigint, point: ProjPointType<bigint>): Promise<CryptoKey> {
const jwk = {
crv: 'P-521',
d: bg2b64url(d),
ext: false,
key_ops: ['sign'],
kty: 'EC',
x: bg2b64url(point.x),
y: bg2b64url(point.y)
}
return crypto.subtle.importKey(
'jwk',
jwk,
{
name: 'ECDSA',
namedCurve: 'P-521'
},
false,
['sign']
)
}
function crypto_pubkey(point: ProjPointType<bigint>) : Promise<CryptoKey> {
const jwk = {
crv: "P-521",
ext: true,
key_ops: [ "verify" ],
kty: "EC",
x: bg2b64url(point.x),
y: bg2b64url(point.y),
}
return crypto.subtle.importKey(
"jwk",
jwk,
{
name: "ECDSA",
namedCurve: "P-521"
},
true,
["verify"]
)
function crypto_pubkey(point: ProjPointType<bigint>): Promise<CryptoKey> {
const jwk = {
crv: 'P-521',
ext: true,
key_ops: ['verify'],
kty: 'EC',
x: bg2b64url(point.x),
y: bg2b64url(point.y)
}
return crypto.subtle.importKey(
'jwk',
jwk,
{
name: 'ECDSA',
namedCurve: 'P-521'
},
true,
['verify']
)
}
function bg2b64url(n: bigint) : string {
return encode(btoa(String.fromCharCode(...hexToBytes(numberToHexUnpadded(n)))))
function bg2b64url(n: bigint): string {
return encode(btoa(String.fromCharCode(...hexToBytes(numberToHexUnpadded(n)))))
}
function encode(input: string) : string {
// Replace non-url compatible chars with base64 standard chars
input = input
.replace(/\+/g, '-')
.replace(/\//g, '_')
function encode(input: string): string {
// Replace non-url compatible chars with base64 standard chars
input = input.replace(/\+/g, '-').replace(/\//g, '_')
const last = input.lastIndexOf("=")
if (last > 0) return input.substring(0, last)
return input
const last = input.lastIndexOf('=')
if (last > 0) return input.substring(0, last)
return input
}
function a2bg(data: Uint8Array): bigint {
return hexToNumber(bytesToHex(data))
}
function a2bg(data: Uint8Array) : bigint {
return hexToNumber(bytesToHex(data))
}
+31 -31
View File
@@ -1,38 +1,38 @@
import logger from 'log'
const log = logger('crypto:signature')
export async function gen_keypair(extractable : boolean = true) : Promise<CryptoKeyPair> {
return crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-521",
},
extractable,
["sign", "verify"]
)
export async function gen_keypair(extractable: boolean = true): Promise<CryptoKeyPair> {
return crypto.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-521'
},
extractable,
['sign', 'verify']
)
}
export {derive_keypair} from './signature.derive'
export async function sign(message: Uint8Array, privkey: CryptoKey) : Promise<Uint8Array> {
log.trace("sign")
const buffer = await crypto.subtle.sign(
{
name: "ECDSA",
hash: "SHA-512"
},
privkey,
message
)
return new Uint8Array(buffer)
export async function sign(message: Uint8Array, privkey: CryptoKey): Promise<Uint8Array> {
log.trace('sign')
const buffer = await crypto.subtle.sign(
{
name: 'ECDSA',
hash: 'SHA-512'
},
privkey,
message
)
return new Uint8Array(buffer)
}
export async function verify(message: Uint8Array, pubkey: CryptoKey, signature: Uint8Array) : Promise<boolean> {
log.trace("verify")
return crypto.subtle.verify(
{
name: "ECDSA",
hash: "SHA-512"
},
pubkey,
signature,
message,
)
export async function verify(message: Uint8Array, pubkey: CryptoKey, signature: Uint8Array): Promise<boolean> {
log.trace('verify')
return crypto.subtle.verify(
{
name: 'ECDSA',
hash: 'SHA-512'
},
pubkey,
signature,
message
)
}