Notes/static/js/crypto.mjs
2023-07-01 20:33:26 +02:00

70 lines
1.8 KiB
JavaScript

export default class Crypto {
constructor(key) {
if(typeof key === 'string')
this.key = sjcl.codec.base64.toBits(base64_key)
else
this.key = key
this.aes = new sjcl.cipher.aes(this.key)
}
static generate_key() {
return sjcl.random.randomWords(8)
}
static pass_to_key(pass, salt = null) {
if(salt === null)
salt = sjcl.random.randomWords(4) // 128 bits (16 bytes)
let key = sjcl.misc.pbkdf2(pass, salt, 10000)
return {
salt,
key,
}
}
encrypt(plaintext_data_in_bits, counter, return_encoded = true) {
// 8 bytes of random data, (1 word = 4 bytes) * 2
// with 8 bytes of byte encoded counter is used as
// IV to guarantee a non-repeated IV (which is a catastrophe).
// Assumes counter value is kept unique. Counter is taken from
// Postgres sequence.
let random_bits = sjcl.random.randomWords(2)
let iv_bytes = sjcl.codec.bytes.fromBits(random_bits)
for (let i = 0; i < 8; ++i) {
let mask = 0xffn << BigInt(i*8)
let counter_i_byte = (counter & mask) >> BigInt(i*8)
iv_bytes[15-i] = Number(counter_i_byte)
}
let iv = sjcl.codec.bytes.toBits(iv_bytes)
let encrypted = sjcl.mode['ccm'].encrypt(
this.aes,
plaintext_data_in_bits,
iv,
)
// Returning 16 bytes (4 words) IV + encrypted data.
if(return_encoded)
return sjcl.codec.base64.fromBits(
iv.concat(encrypted)
)
else
return iv.concat(encrypted)
}
decrypt(encrypted_base64_data) {
try {
let encoded = sjcl.codec.base64.toBits(encrypted_base64_data)
let iv = encoded.slice(0, 4) // in words (4 bytes), not bytes
let encrypted_data = encoded.slice(4)
return sjcl.mode['ccm'].decrypt(this.aes, encrypted_data, iv)
} catch(err) {
if(err.message == `ccm: tag doesn't match`)
throw('Decryption failed')
else
throw(err)
}
}
}