Implement boxes

This commit is contained in:
2024-05-21 16:00:16 +02:00
parent e515c80f36
commit d845ae1391
15 changed files with 920 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import {Result} from 'rust'
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 _phantom!: T;
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",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: {name: "SHA-512"}
},
extractable,
["encrypt", "decrypt"]
)
return [keys.privateKey, keys.publicKey]
}
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(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(e) {
log.warn('decryption failed')
log.debug(`error : ${e}`)
}
return Result.error([])
}
public toString() : string {
log.trace('toString')
return misc.a2b64(new Uint8Array(this.cipher))
}
public static fromString<T>(data: string) : Result<PrivateBox<T>> {
log.trace('fromString')
const res = misc.b642a(data)
if (res.is_err()) return Result.error([])
return Result.ok(new PrivateBox(res.unwrap()))
}
}
+127
View File
@@ -0,0 +1,127 @@
import {Result} from 'rust'
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 constructor(
private readonly wrapped_key: Uint8Array,
private readonly algorithm: Algorithm,
private readonly usage: KeyUsage[],
private readonly format: KeyFormat
) {}
/**
* Create a new keypair for signing
* @param extractable if the keys must be extractable or not
* @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",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: {name: "SHA-512"},
},
extractable,
["wrapKey", "unwrapKey"]
)
return [key.privateKey, key.publicKey]
}
/**
* Wrap a key using a public key
*
* Kinda makes sense, but it can't wrap an RSA encryption key
* @param pubkey used to wrap
* @param key to wrap
* @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 {
const wrapped = await window.crypto.subtle.wrapKey(
format,
key,
pubkey,
algorithm
)
return Result.ok(new PrivateWrap(new Uint8Array(wrapped), key.algorithm, key.usages, format))
} catch (e) {
log.warn('wrap failed')
log.debug(`error : ${e}`)
}
return Result.error([])
}
/**
* Unwrap a key using a private key
* @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,
this.wrapped_key,
privkey,
algorithm,
this.algorithm,
true,
this.usage
)
return Result.ok(key)
} catch(e) {
log.warn('unwrap failed')
log.debug(`error : ${e}`)
}
return Result.error([])
}
public toString() : string {
log.trace('toString')
const wrapped_key = misc.a2b64(this.wrapped_key)
return JSON.stringify({
wrapped_key,
algorithm: this.algorithm,
usage: this.usage,
format: this.format,
})
}
public static fromString(data: string) : Result<PrivateWrap> {
log.trace('fromString')
try {
const {wrapped_key: wrapped_key64, algorithm, usage, format}: {wrapped_key: string, algorithm: Algorithm, usage: KeyUsage[], format: KeyFormat} = JSON.parse(data)
const key = misc.b642a(wrapped_key64)
if (key.is_err()) return Result.error([])
return Result.ok(new PrivateWrap(key.unwrap(), algorithm, usage, format))
} catch (e) {
log.warn('parsing failed')
log.debug(`error : ${e}`)
}
return Result.error([])
}
}
+60
View File
@@ -0,0 +1,60 @@
import {Result} from 'rust'
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(
private readonly secret_wrap: SecretWrap,
private readonly salt: Uint8Array,
) {}
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)
const box = await SecretWrap.wrap_key(key, key_to_wrap)
if (box.is_err()) return Result.error([])
return Result.ok(new PwdWrap(box.unwrap(), salt))
}
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)
if (unwrapped_key.is_err()) return Result.error([])
return Result.ok(unwrapped_key.unwrap())
}
private static async get_key(pwd: string, salt: Uint8Array) : Promise<CryptoKey> {
return pbkdf(pwd, salt, ['wrapKey', 'unwrapKey'])
}
public toString() : string {
const salt = misc.a2b64(this.salt)
const box = this.secret_wrap.toString()
return `${salt}${box}`
}
public static fromString(data: string) : Result<PwdWrap> {
log.trace('fromString')
const salt64 = data.slice(0, 24)
const salt = misc.b642a(salt64)
if (salt.is_err()) return Result.error([])
const box64 = data.slice(24)
const box = SecretWrap.fromString(box64)
if (box.is_err()) return Result.error([])
return Result.ok(new PwdWrap(box.unwrap(), salt.unwrap()))
}
}
+54
View File
@@ -0,0 +1,54 @@
import {Result} from 'rust'
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 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.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>> {
log.trace('decrypt')
const key = await PwdBox.get_key(pwd, this.salt)
return this.secret_box.decrypt(key)
}
private static async get_key(pwd: string, salt: Uint8Array) : Promise<CryptoKey> {
return pbkdf(pwd, salt, ["encrypt", "decrypt"])
}
public toString() : string {
const salt = misc.a2b64(this.salt)
const box = this.secret_box.toString()
return `${salt}${box}`
}
public static fromString<T>(data: string) : Result<PwdBox<T>> {
log.trace('fromString')
const salt64 = data.slice(0, 24)
const salt = misc.b642a(salt64)
if (salt.is_err()) return Result.error([])
const box64 = data.slice(24)
const box = SecretBox.fromString<T>(box64)
if (box.is_err()) return Result.error([])
return Result.ok(new PwdBox(box.unwrap(), salt.unwrap()))
}
}
+110
View File
@@ -0,0 +1,110 @@
import {Result} from 'rust'
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,
private readonly algorithm: Algorithm,
private readonly usage: KeyUsage[],
private readonly format: KeyFormat,
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,
['wrapKey', 'unwrapKey']
)
}
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))
try {
const wrapped_key = await crypto.subtle.wrapKey(
format,
key_to_wrap,
wrapping_key,
{
name: 'AES-GCM',
iv,
}
)
return Result.ok(new SecretWrap(new Uint8Array(wrapped_key), key_to_wrap.algorithm, key_to_wrap.usages, format, iv))
} 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,
this.wrapped_key,
wrapping_key,
{
name: 'AES-GCM',
iv: this.iv,
},
this.algorithm,
true,
this.usage,
)
return Result.ok(key)
} catch (e) {
log.warn('Unwrapping failed')
log.debug(`Error : ${e}`)
}
return Result.error([])
}
public toString() : string {
const wrapped_key = misc.a2b64(this.wrapped_key)
const iv = misc.a2b64(this.iv)
return JSON.stringify({
wrapped_key,
algorithm: this.algorithm,
usage: this.usage,
format: this.format,
iv,
})
}
public static fromString(s: string) : Result<SecretWrap> {
log.trace('fromString')
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.is_err()) return Result.error([])
const wrapped_key = misc.b642a(wrapped_key64)
if (wrapped_key.is_err()) return Result.error([])
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([])
}
}
+87
View File
@@ -0,0 +1,87 @@
import {Result} from 'rust'
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 phantom!: T
private constructor(
private readonly iv: Uint8Array,
private readonly cipher: Uint8Array
) {}
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",
length: 256,
},
extractable,
["encrypt", "decrypt"]
)
}
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 (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 (e) {
log.warn('Decryption failed')
log.debug(`Error : ${e}`)
}
return Result.error([])
}
public toString() : string {
const iv = misc.a2b64(this.iv)
const cipher = misc.a2b64(this.cipher)
return `${iv}.${cipher}`
}
public static fromString<T>(data: string) : Result<SecretBox<T>> {
log.trace('fromString')
const parts = data.split(".")
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])
if (iv.is_err()) return Result.error([])
if (cipher.is_err()) return Result.error([])
return Result.ok(new SecretBox(iv.unwrap(), cipher.unwrap()))
}
}