@@ -17,6 +17,7 @@
|
||||
"jose": "^5.3.0",
|
||||
"result": "git+git@git.pband.ch:typescript/result",
|
||||
"misc": "git+git@git.pband.ch:typescript/misc",
|
||||
"log": "git+git@git.pband.ch:typescript/log",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+20
-6
@@ -1,21 +1,25 @@
|
||||
import {Result} from 'result'
|
||||
import * as misc from 'misc'
|
||||
import logger from 'log'
|
||||
|
||||
export type PubKey = CryptoKey
|
||||
export type PrivKey = CryptoKey
|
||||
export type KeyPair = [PrivKey, PubKey]
|
||||
|
||||
const algorithm: RsaOaepParams = {name: "RSA-OAEP"}
|
||||
const log = logger("crypto:boxes:private")
|
||||
|
||||
export class PrivateBox<T> {
|
||||
readonly cipher: Uint8Array;
|
||||
readonly _phantom!: T;
|
||||
|
||||
private constructor(cipher: Uint8Array) {
|
||||
this.cipher = cipher
|
||||
}
|
||||
private constructor(
|
||||
private readonly cipher: Uint8Array
|
||||
) {}
|
||||
|
||||
public static async gen_keypair(extractable: boolean = false): Promise<KeyPair> {
|
||||
log.trace('generate keypair')
|
||||
log.debug('extractable :', extractable ? 'yes' : 'no')
|
||||
|
||||
const keys = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
@@ -31,18 +35,28 @@ export class PrivateBox<T> {
|
||||
}
|
||||
|
||||
public static async encrypt<T>(key: PubKey, data: Uint8Array): Promise<Result<PrivateBox<T>>> {
|
||||
log.trace('encrypt')
|
||||
|
||||
try {
|
||||
const cipher = await window.crypto.subtle.encrypt(algorithm, key, data)
|
||||
return Result.ok(new PrivateBox(new Uint8Array(cipher)))
|
||||
} catch(_) {}
|
||||
} catch(e) {
|
||||
log.warn('encryption failed')
|
||||
log.debug(`error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
public async decrypt(key: PrivKey): Promise<Result<Uint8Array>> {
|
||||
log.trace('decrypt')
|
||||
|
||||
try {
|
||||
const plain = await window.crypto.subtle.decrypt(algorithm, key, this.cipher)
|
||||
return Result.ok(new Uint8Array(plain))
|
||||
} catch(_) {}
|
||||
} catch(e) {
|
||||
log.warn('decryption failed')
|
||||
log.debug(`error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
|
||||
+27
-14
@@ -1,24 +1,21 @@
|
||||
import {Result} from 'result'
|
||||
import * as misc from 'misc'
|
||||
import logger from 'log'
|
||||
|
||||
export type PubKey = CryptoKey
|
||||
export type PrivKey = CryptoKey
|
||||
export type KeyPair = [PrivKey, PubKey]
|
||||
|
||||
const algorithm: RsaOaepParams = {name: "RSA-OAEP"}
|
||||
const log = logger("crypto:boxes:private-wrap")
|
||||
|
||||
export class PrivateWrap {
|
||||
private readonly wrapped_key: Uint8Array
|
||||
private readonly usage: KeyUsage[]
|
||||
private readonly algorithm: Algorithm
|
||||
private readonly format: KeyFormat
|
||||
|
||||
private constructor(key: Uint8Array, algorithm: Algorithm, usage: KeyUsage[], format: KeyFormat) {
|
||||
this.wrapped_key = key
|
||||
this.algorithm = algorithm
|
||||
this.usage = usage
|
||||
this.format = format
|
||||
}
|
||||
private constructor(
|
||||
private readonly wrapped_key: Uint8Array,
|
||||
private readonly algorithm: Algorithm,
|
||||
private readonly usage: KeyUsage[],
|
||||
private readonly format: KeyFormat
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new keypair for signing
|
||||
@@ -26,6 +23,9 @@ export class PrivateWrap {
|
||||
* @return [privkey, pubkey] keys
|
||||
*/
|
||||
public static async gen_key(extractable: boolean = false) : Promise<KeyPair> {
|
||||
log.trace('generate keypair')
|
||||
log.debug('extractable :', extractable ? 'yes' : 'no')
|
||||
|
||||
let key = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
@@ -49,6 +49,8 @@ export class PrivateWrap {
|
||||
* @return an error if the key is not extractable
|
||||
*/
|
||||
public static async wrap(pubkey: PubKey, key: CryptoKey) : Promise<Result<PrivateWrap>> {
|
||||
log.trace('wrap key')
|
||||
|
||||
const format = key.type === "secret" ? "raw" : "pkcs8"
|
||||
|
||||
try {
|
||||
@@ -60,7 +62,10 @@ export class PrivateWrap {
|
||||
)
|
||||
|
||||
return Result.ok(new PrivateWrap(new Uint8Array(wrapped), key.algorithm, key.usages, format))
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
log.warn('wrap failed')
|
||||
log.debug(`error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
@@ -70,6 +75,8 @@ export class PrivateWrap {
|
||||
* @param privkey used to unwrap
|
||||
*/
|
||||
public async unwrap(privkey: PrivKey) : Promise<Result<CryptoKey>> {
|
||||
log.trace('unwrap key')
|
||||
|
||||
try {
|
||||
const key = await window.crypto.subtle.unwrapKey(
|
||||
this.format,
|
||||
@@ -81,7 +88,10 @@ export class PrivateWrap {
|
||||
this.usage
|
||||
)
|
||||
return Result.ok(key)
|
||||
} catch(_) {}
|
||||
} catch(e) {
|
||||
log.warn('unwrap failed')
|
||||
log.debug(`error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
@@ -103,7 +113,10 @@ export class PrivateWrap {
|
||||
if (key.error()) return Result.error([])
|
||||
|
||||
return Result.ok(new PrivateWrap(key.unwrap(), algorithm, usage, format))
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
log.warn('parsing failed')
|
||||
log.debug(`error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import {Result} from 'result'
|
||||
import * as misc from 'misc'
|
||||
import {SecretWrap} from './secret-wrap'
|
||||
import {pbkdf} from '../pbkdf'
|
||||
import logger from 'log'
|
||||
|
||||
const log = logger('crypto:boxes:pwd-wrap')
|
||||
|
||||
export class PwdWrap {
|
||||
private constructor(
|
||||
@@ -10,6 +13,8 @@ export class PwdWrap {
|
||||
) {}
|
||||
|
||||
public static async wrap(pwd: string, key_to_wrap: CryptoKey) : Promise<Result<PwdWrap>> {
|
||||
log.trace('wrap key')
|
||||
|
||||
const salt = crypto.getRandomValues(new Uint8Array(18))
|
||||
const key = await PwdWrap.get_key(pwd, salt)
|
||||
|
||||
@@ -20,6 +25,8 @@ export class PwdWrap {
|
||||
}
|
||||
|
||||
public async unwrap(pwd: string) : Promise<Result<CryptoKey>> {
|
||||
log.trace('unwrap key')
|
||||
|
||||
const key = await PwdWrap.get_key(pwd, this.salt)
|
||||
|
||||
const unwrapped_key = await this.secret_wrap.unwrap(key)
|
||||
|
||||
+17
-11
@@ -2,30 +2,34 @@ import {Result} from 'result'
|
||||
import * as misc from 'misc'
|
||||
import {SecretBox} from './symmetric'
|
||||
import {pbkdf} from '../pbkdf'
|
||||
import logger from 'log'
|
||||
|
||||
const log = logger('crypto:boxes:pwd')
|
||||
|
||||
export class PwdBox<T> {
|
||||
private readonly secret_box: SecretBox<T>
|
||||
private readonly salt: Uint8Array
|
||||
|
||||
private constructor(secret_box: SecretBox<T>, salt: Uint8Array) {
|
||||
this.secret_box = secret_box
|
||||
this.salt = salt
|
||||
}
|
||||
private constructor(
|
||||
private readonly secret_box: SecretBox<T>,
|
||||
private readonly salt: Uint8Array
|
||||
) {}
|
||||
|
||||
public static async encrypt<T>(pwd: string, data: Uint8Array): Promise<PwdBox<T>> {
|
||||
log.trace('encrypt')
|
||||
|
||||
const salt = crypto.getRandomValues(new Uint8Array(18))
|
||||
const key = await PwdBox.gen_key(pwd, salt)
|
||||
const key = await PwdBox.get_key(pwd, salt)
|
||||
|
||||
const box = (await SecretBox.encrypt<T>(key, data)).unwrap() // I just created the key, I control it
|
||||
|
||||
return new PwdBox(box, salt)
|
||||
}
|
||||
public async decrypt(pwd: string): Promise<Result<Uint8Array>> {
|
||||
const key = await PwdBox.gen_key(pwd, this.salt)
|
||||
log.trace('decrypt')
|
||||
const key = await PwdBox.get_key(pwd, this.salt)
|
||||
return this.secret_box.decrypt(key)
|
||||
}
|
||||
|
||||
private static async gen_key(pwd: string, salt: Uint8Array) : Promise<CryptoKey> {
|
||||
private static async get_key(pwd: string, salt: Uint8Array) : Promise<CryptoKey> {
|
||||
return pbkdf(pwd, salt, ["encrypt", "decrypt"])
|
||||
}
|
||||
|
||||
@@ -35,10 +39,12 @@ export class PwdBox<T> {
|
||||
return `${salt}${box}`
|
||||
}
|
||||
public static fromString<T>(data: string) : Result<PwdBox<T>> {
|
||||
const salt = misc.b642a(data.slice(0, 24))
|
||||
const salt64 = data.slice(0, 24)
|
||||
const salt = misc.b642a(salt64)
|
||||
if (salt.error()) return Result.error([])
|
||||
|
||||
const box = SecretBox.fromString<T>(data.slice(24))
|
||||
const box64 = data.slice(24)
|
||||
const box = SecretBox.fromString<T>(box64)
|
||||
if (box.error()) return Result.error([])
|
||||
|
||||
return Result.ok(new PwdBox(box.unwrap(), salt.unwrap()))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {Result} from 'result'
|
||||
import * as misc from 'misc'
|
||||
import logger from 'log'
|
||||
|
||||
export type Key = CryptoKey
|
||||
|
||||
const log = logger('crypto:boxes:secret-wrap')
|
||||
|
||||
export class SecretWrap {
|
||||
private constructor(
|
||||
private readonly wrapped_key: Uint8Array,
|
||||
@@ -12,6 +15,9 @@ export class SecretWrap {
|
||||
private readonly iv: Uint8Array) {}
|
||||
|
||||
public static async gen_key(extractable: boolean = false) : Promise<Key> {
|
||||
log.trace('Generating key')
|
||||
log.debug('Extractable :', extractable ? 'yes' : 'no')
|
||||
|
||||
return crypto.subtle.generateKey(
|
||||
{name: 'AES-GCM', length: 256},
|
||||
extractable,
|
||||
@@ -20,6 +26,8 @@ export class SecretWrap {
|
||||
}
|
||||
|
||||
public static async wrap_key(wrapping_key: Key, key_to_wrap: CryptoKey) : Promise<Result<SecretWrap>> {
|
||||
log.trace('wrap key')
|
||||
|
||||
const format = key_to_wrap.type === "secret" ? "raw" : "pkcs8"
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12))
|
||||
|
||||
@@ -35,12 +43,17 @@ export class SecretWrap {
|
||||
)
|
||||
|
||||
return Result.ok(new SecretWrap(new Uint8Array(wrapped_key), key_to_wrap.algorithm, key_to_wrap.usages, format, iv))
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
log.warn('Wrapping failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
|
||||
public async unwrap(wrapping_key: Key) : Promise<Result<CryptoKey>> {
|
||||
log.trace('unwrap key')
|
||||
|
||||
try {
|
||||
const key = await crypto.subtle.unwrapKey(
|
||||
this.format,
|
||||
@@ -56,7 +69,10 @@ export class SecretWrap {
|
||||
)
|
||||
|
||||
return Result.ok(key)
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
log.warn('Unwrapping failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
@@ -73,13 +89,20 @@ export class SecretWrap {
|
||||
})
|
||||
}
|
||||
public static fromString(s: string) : Result<SecretWrap> {
|
||||
const {iv: iv64, wrapped_key: wrapped_key64, algorithm, usage, format}: {wrapped_key: string, algorithm: Algorithm, usage: KeyUsage[], format: KeyFormat, iv: string} = JSON.parse(s)
|
||||
try {
|
||||
const {iv: iv64, wrapped_key: wrapped_key64, algorithm, usage, format}: { wrapped_key: string, algorithm: Algorithm, usage: KeyUsage[], format: KeyFormat, iv: string } = JSON.parse(s)
|
||||
|
||||
const iv = misc.b642a(iv64)
|
||||
if (iv.error()) return Result.error([])
|
||||
const wrapped_key = misc.b642a(wrapped_key64)
|
||||
if (wrapped_key.error()) return Result.error([])
|
||||
const iv = misc.b642a(iv64)
|
||||
if (iv.error()) return Result.error([])
|
||||
const wrapped_key = misc.b642a(wrapped_key64)
|
||||
if (wrapped_key.error()) return Result.error([])
|
||||
|
||||
return Result.ok(new SecretWrap(wrapped_key.unwrap(), algorithm, usage, format, iv.unwrap()))
|
||||
return Result.ok(new SecretWrap(wrapped_key.unwrap(), algorithm, usage, format, iv.unwrap()))
|
||||
} catch(e) {
|
||||
log.warn('Parsing fromString failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
}
|
||||
|
||||
+23
-3
@@ -1,8 +1,11 @@
|
||||
import {Result} from 'result'
|
||||
import * as misc from 'misc'
|
||||
import logger from 'log'
|
||||
|
||||
export type Key = CryptoKey
|
||||
|
||||
const log = logger("crypto:boxes:secret")
|
||||
|
||||
export class SecretBox<T> {
|
||||
private readonly cipher: Uint8Array
|
||||
private readonly iv: Uint8Array
|
||||
@@ -14,6 +17,9 @@ export class SecretBox<T> {
|
||||
}
|
||||
|
||||
public static async gen_key(extractable: boolean = false): Promise<Key> {
|
||||
log.trace('Generate key')
|
||||
log.debug('Extractable :', extractable ? 'yes' : 'no')
|
||||
|
||||
return window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
@@ -25,24 +31,34 @@ export class SecretBox<T> {
|
||||
}
|
||||
|
||||
public static async encrypt<T>(key: Key, data: Uint8Array): Promise<Result<SecretBox<T>>> {
|
||||
log.trace('Encrypt data')
|
||||
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12))
|
||||
const algorithm = {name: "AES-GCM", iv}
|
||||
|
||||
try {
|
||||
const cipher = await window.crypto.subtle.encrypt(algorithm, key, data)
|
||||
return Result.ok(new SecretBox(iv, new Uint8Array(cipher)))
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
log.warn('Encryption failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
public async decrypt(key: Key): Promise<Result<Uint8Array>> {
|
||||
log.trace('Decrypt data')
|
||||
|
||||
const algorithm = {name: "AES-GCM", iv: this.iv}
|
||||
|
||||
try {
|
||||
const cipher = await window.crypto.subtle.decrypt(algorithm, key, this.cipher)
|
||||
const buffer = new Uint8Array(cipher)
|
||||
return Result.ok(buffer)
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
log.warn('Decryption failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
@@ -54,7 +70,11 @@ export class SecretBox<T> {
|
||||
}
|
||||
public static fromString<T>(data: string) : Result<SecretBox<T>> {
|
||||
const parts = data.split(".")
|
||||
if (parts.length !== 2) return Result.error([])
|
||||
if (parts.length !== 2) {
|
||||
log.warn("Invalid secret box, part count doesn't match")
|
||||
log.debug(`Count : ${parts.length}`)
|
||||
return Result.error([])
|
||||
}
|
||||
|
||||
const iv = misc.b642a(parts[0])
|
||||
const cipher = misc.b642a(parts[1])
|
||||
|
||||
+21
-1
@@ -1,5 +1,8 @@
|
||||
import {Result} from 'result'
|
||||
import * as jose from 'jose'
|
||||
import logger from 'log'
|
||||
|
||||
const log = logger('crypto:jwt')
|
||||
|
||||
export type JWTstring = `${string}.${string}.${string}`
|
||||
export type JWTalgorithm = "HS256" | "HS512" | "ES256" | "ES512" | "EdDSA"
|
||||
@@ -14,6 +17,9 @@ export class JWTcontext {
|
||||
) {}
|
||||
|
||||
public static async gen_key(alg: JWTalgorithm, extractable: boolean = false) : Promise<Key | KeyPair> {
|
||||
log.trace(`Generate new ${alg} key`)
|
||||
log.debug('Key extractable :', extractable ? 'yes' : 'no')
|
||||
|
||||
switch (alg) {
|
||||
case "HS256":
|
||||
case "HS512":
|
||||
@@ -29,6 +35,13 @@ export class JWTcontext {
|
||||
}
|
||||
|
||||
public async sign<T extends jose.JWTPayload>(message: T, set_issued: boolean = false, exp?: number | string | Date, audience?: string | string[], issuer?: string): Promise<JWT<T>> {
|
||||
log.trace('sign JWT')
|
||||
log.debug('Config :', {
|
||||
set_issued,
|
||||
exp,
|
||||
issuer,
|
||||
})
|
||||
|
||||
let jwt = new jose.SignJWT({payload: message}).setProtectedHeader({ alg: this.alg })
|
||||
|
||||
if (set_issued) jwt = jwt.setIssuedAt()
|
||||
@@ -43,12 +56,18 @@ export class JWTcontext {
|
||||
}
|
||||
|
||||
public async verify<T>(jwt: JWT<T>, audience?: string | string[], issuer?: string | string[]): Promise<Result<T>> {
|
||||
log.trace('Verify JWT')
|
||||
log.debug('Issuers :', issuer)
|
||||
|
||||
const key = this.get_key(false)
|
||||
|
||||
try {
|
||||
let payload = await jose.jwtVerify(jwt.jwt, key)
|
||||
return Result.ok(payload.payload.payload as T)
|
||||
} catch(_) {}
|
||||
} catch(e) {
|
||||
log.warn('JWT verification failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
@@ -72,6 +91,7 @@ export class JWT<T> {
|
||||
) {}
|
||||
|
||||
public get payload() : T {
|
||||
log.trace("Decode payload of JWT")
|
||||
return jose.decodeJwt(this.jwt).payload as T
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import logger from 'log'
|
||||
|
||||
const log = logger('crypto:pbkdf')
|
||||
|
||||
export async function pbkdf(password: string, salt: Uint8Array, usages: KeyUsage[]): Promise<CryptoKey> {
|
||||
log.trace('derive')
|
||||
log.debug(`Usages : ${usages}`)
|
||||
|
||||
const keyMaterial = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(password),
|
||||
|
||||
+17
-2
@@ -1,4 +1,5 @@
|
||||
import {Result} from 'result'
|
||||
import logger from 'log'
|
||||
|
||||
export type PrivKey = CryptoKey
|
||||
export type PubKey = CryptoKey
|
||||
@@ -8,6 +9,7 @@ const algorithm: EcdsaParams = {
|
||||
name: "ECDSA",
|
||||
hash: {name: "SHA-512"},
|
||||
}
|
||||
const log = logger('crypto:signature')
|
||||
|
||||
/**
|
||||
* Create a new keypair for signing
|
||||
@@ -15,6 +17,9 @@ const algorithm: EcdsaParams = {
|
||||
* @return [privkey, pubkey] keys
|
||||
*/
|
||||
export async function gen_keypair(extractable: boolean = false) : Promise<KeyPair> {
|
||||
log.trace('Generating keypair')
|
||||
log.debug('Extractable :', extractable ? 'yes' : 'no')
|
||||
|
||||
let key = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "ECDSA",
|
||||
@@ -27,18 +32,25 @@ export async function gen_keypair(extractable: boolean = false) : Promise<KeyPai
|
||||
return [key.privateKey, key.publicKey]
|
||||
}
|
||||
export async function sign(privkey: PrivKey, message: Uint8Array) : Promise<Result<ArrayBuffer>> {
|
||||
log.trace('sign')
|
||||
|
||||
try {
|
||||
return Result.ok(await window.crypto.subtle.sign(
|
||||
algorithm,
|
||||
privkey,
|
||||
message,
|
||||
))
|
||||
} catch(_) {}
|
||||
} catch(e) {
|
||||
log.warn('Signature failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
|
||||
export async function verify(pubkey: PubKey, message: Uint8Array, signature: ArrayBuffer): Promise<boolean> {
|
||||
log.trace('Verify signature')
|
||||
|
||||
try {
|
||||
return await window.crypto.subtle.verify(
|
||||
algorithm,
|
||||
@@ -46,7 +58,10 @@ export async function verify(pubkey: PubKey, message: Uint8Array, signature: Arr
|
||||
signature,
|
||||
message
|
||||
);
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
log.warn('Verification failed')
|
||||
log.debug(`Error : ${e}`)
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user