Resolve "Move Hasher and Encryptor to Primitives"

This commit is contained in:
2025-04-07 03:37:43 +00:00
committed by Jim Hamill
parent 653fbffac7
commit 9c7adae7b7
13 changed files with 118 additions and 126 deletions
+130
View File
@@ -0,0 +1,130 @@
// TODO: Consider whether we should shuffle this to Stack Util (as opposed to Primitive).
// It is async which breaks from our Primitive patterns.
import { generateRandomBytes, 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),
// NOTE: IV of length 96 bits (12 bytes) is recommended length as per https://csrc.nist.gov/pubs/sp/800/38/d/final.
// This is both secure - and avoids additional compute incurred by hashing.
ivLength: 12,
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);
}
static async generateRandom(opts: Partial<EncrypterOpts> = {}) {
const key = generateRandomBytes(32);
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);
}
}
+137
View File
@@ -0,0 +1,137 @@
import {
isHex,
isBase64,
binToHex,
base64ToBin,
hexToBin,
utf8ToBin,
ripemd160,
sha256,
} from '@bitauth/libauth';
/**
* Represents a hash primitive that provides various cryptographic hashing capabilities.
* This class handles conversions between different formats (bytes, hex, UTF-8) and
* supports multiple hashing algorithms (SHA256, RIPEMD160, Hash160, Hash256).
*/
export class Hasher {
/**
* Creates a new Hash instance.
* @param bytes - The byte array representation of the data to be hashed
*/
constructor(public bytes: Uint8Array) {}
/**
* Creates a Hash instance from a byte array.
* @param bytes - The byte array to create the hash from
* @returns A new Hash instance containing the provided bytes
*/
public static fromBytes(bytes: Uint8Array) {
return new this(bytes);
}
/**
* Creates a Hash instance from a Base64 string.
* @param base64 - The Base64 string to create the hash from
* @returns A new Hash instance containing the bytes converted from the Base64 string
* @throws {Error} If the provided string is not valid Base64
*/
public static fromBase64(base64: string) {
// Ensure that the given string is base64.
if (!isBase64(base64)) {
throw new Error('The given string is not valid Base64.');
}
return new this(base64ToBin(base64));
}
/**
* Creates a Hash instance from a hexadecimal string.
* @param hex - The hexadecimal string to create the hash from
* @returns A new Hash instance containing the bytes converted from the hex string
* @throws {Error} If the provided string is not valid hexadecimal
*/
public static fromHex(hex: string) {
// Ensure that the given string is hex.
if (!isHex(hex)) {
throw new Error('The given string is not valid hexadecimal.');
}
return new this(hexToBin(hex));
}
/**
* Creates a Hash instance from a UTF-8 string.
* @param utf8 - The UTF-8 string to create the hash from
* @returns A new Hash instance containing the bytes converted from the UTF-8 string
*/
public static fromUTF8(utf8: string) {
return new this(utf8ToBin(utf8));
}
/**
* Computes the SHA256 hash of the stored bytes.
* @returns A Uint8Array containing the SHA256 hash
*/
public toSHA256Bytes(): Uint8Array {
return sha256.hash(this.bytes);
}
/**
* Computes the SHA256 hash of the stored bytes and returns it as a hexadecimal string.
* @returns The SHA256 hash as a hexadecimal string
*/
public toSHA256Hex(): string {
return binToHex(this.toSHA256Bytes());
}
/**
* Computes the RIPEMD160 hash of the stored bytes.
* @returns A Uint8Array containing the RIPEMD160 hash
*/
public toRIPEMD160Bytes(): Uint8Array {
return ripemd160.hash(this.bytes);
}
/**
* Computes the RIPEMD160 hash of the stored bytes and returns it as a hexadecimal string.
* @returns The RIPEMD160 hash as a hexadecimal string
*/
public toRIPEMD160Hex(): string {
return binToHex(this.toRIPEMD160Bytes());
}
/**
* Computes the Hash160 (RIPEMD160(SHA256(x))) of the stored bytes.
* This is commonly used in cryptocurrency addresses.
* @returns A Uint8Array containing the Hash160 value
*/
public toHash160Bytes(): Uint8Array {
return ripemd160.hash(this.toSHA256Bytes());
}
/**
* Computes the Hash160 (RIPEMD160(SHA256(x))) of the stored bytes and returns it as a hexadecimal string.
* @returns The Hash160 value as a hexadecimal string
*/
public toHash160Hex(): string {
return binToHex(this.toHash160Bytes());
}
/**
* Computes the Hash256 (SHA256(SHA256(x))) of the stored bytes.
* This is commonly used for computing block hashes in Bitcoin.
* @returns A Uint8Array containing the Hash256 value
*/
public toHash256Bytes(): Uint8Array {
return sha256.hash(this.toSHA256Bytes());
}
/**
* Computes the Hash256 (SHA256(SHA256(x))) of the stored bytes and returns it as a hexadecimal string.
* @returns The Hash256 value as a hexadecimal string
*/
public toHash256Hex(): string {
return binToHex(sha256.hash(this.toSHA256Bytes()));
}
}
+2
View File
@@ -1,7 +1,9 @@
export * from './address.js';
export * from './block-header.js';
export * from './bytes.js';
export * from './encrypter.js';
export * from './ext-json.js';
export * from './hasher.js';
export * from './hd-private-node.js';
export * from './hd-public-node.js';
export * from './mnemonic.js';