123 lines
3.2 KiB
TypeScript
123 lines
3.2 KiB
TypeScript
// TODO: Consider whether we should shuffle this to Stack Util (as opposed to Primitive).
|
|
// It is async which breaks from our Primitive patterns.
|
|
|
|
import { utf8ToBin } from '@bitauth/libauth';
|
|
|
|
// NOTE: We need to provide a default salt otherwise we would be deriving a key per each operation.
|
|
// This would be far too computationally expensive.
|
|
// TODO: Allow the salt to be customized in an options argument to the constructor.
|
|
export const DEFAULT_XO_SALT =
|
|
'cash.xo.default_salt_for_encryption_aabbccddeeff00112233445566778899';
|
|
|
|
export type EncrypterOpts = {
|
|
algorithm: string;
|
|
keyLength: number;
|
|
salt: Uint8Array;
|
|
ivLength: number;
|
|
iterations: number;
|
|
};
|
|
|
|
export class Encrypter {
|
|
constructor(
|
|
private key: CryptoKey,
|
|
public opts: EncrypterOpts,
|
|
) {}
|
|
|
|
static async fromBytes(key: Uint8Array, opts: Partial<EncrypterOpts> = {}) {
|
|
// Merge the provided options into our default options.
|
|
const encryptionOpts = {
|
|
algorithm: 'AES-GCM',
|
|
keyLength: 256,
|
|
salt: utf8ToBin(DEFAULT_XO_SALT),
|
|
ivLength: 16,
|
|
iterations: 100_000,
|
|
...opts,
|
|
};
|
|
|
|
// Create a key derivation key
|
|
const keyMaterialBase = await crypto.subtle.importKey(
|
|
'raw',
|
|
key,
|
|
'PBKDF2',
|
|
false,
|
|
['deriveBits', 'deriveKey'],
|
|
);
|
|
|
|
// Derive the actual encryption key
|
|
const derivedKey = await crypto.subtle.deriveKey(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: encryptionOpts.salt,
|
|
iterations: encryptionOpts.iterations,
|
|
hash: 'SHA-256',
|
|
},
|
|
keyMaterialBase,
|
|
{
|
|
name: encryptionOpts.algorithm,
|
|
length: encryptionOpts.keyLength,
|
|
},
|
|
false,
|
|
['encrypt', 'decrypt'],
|
|
);
|
|
|
|
return new Encrypter(derivedKey, encryptionOpts);
|
|
}
|
|
|
|
static async fromPassphrase(
|
|
passphrase: string,
|
|
opts: Partial<EncrypterOpts> = {},
|
|
) {
|
|
const key = utf8ToBin(passphrase);
|
|
|
|
return this.fromBytes(key, opts);
|
|
}
|
|
|
|
async encrypt(payload: Uint8Array | string): Promise<Uint8Array> {
|
|
// Convert string payload to Uint8Array if needed
|
|
const data = typeof payload === 'string' ? utf8ToBin(payload) : payload;
|
|
|
|
// Generate random IV
|
|
const iv = crypto.getRandomValues(new Uint8Array(this.opts.ivLength));
|
|
|
|
// Encrypt the data
|
|
const encryptedData = await crypto.subtle.encrypt(
|
|
{
|
|
name: this.opts.algorithm,
|
|
iv,
|
|
},
|
|
this.key,
|
|
data,
|
|
);
|
|
|
|
// Combine IV and encrypted data
|
|
const result = new Uint8Array(iv.length + encryptedData.byteLength);
|
|
result.set(iv);
|
|
result.set(new Uint8Array(encryptedData), iv.length);
|
|
|
|
return result;
|
|
}
|
|
|
|
async decrypt(payload: Uint8Array | string): Promise<Uint8Array> {
|
|
// Ensure payload is Uint8Array
|
|
if (typeof payload === 'string') {
|
|
throw new Error('Encrypted payload must be Uint8Array');
|
|
}
|
|
|
|
// Extract IV and encrypted data
|
|
const iv = payload.slice(0, this.opts.ivLength);
|
|
const encryptedData = payload.slice(this.opts.ivLength);
|
|
|
|
// Decrypt the data
|
|
const decryptedData = await crypto.subtle.decrypt(
|
|
{
|
|
name: this.opts.algorithm,
|
|
iv,
|
|
},
|
|
this.key,
|
|
encryptedData,
|
|
);
|
|
|
|
return new Uint8Array(decryptedData);
|
|
}
|
|
}
|