Resolve "Move Hasher and Encryptor to Primitives"
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Encrypter, EventEmitter, InverseBloomFilter } from '../utils';
|
||||
|
||||
import { EventEmitter, InverseBloomFilter } from '../utils';
|
||||
import { Encrypter } from '@xocash/primitives';
|
||||
export type StorageOpts = {
|
||||
syncInMemory?: boolean;
|
||||
encryption?: Encrypter;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseStorage, BaseStore, StorageOpts } from './base-store.js';
|
||||
import { StoreInMemorySynced } from './store-inmemory-synced.js';
|
||||
|
||||
import { Encrypter } from '../utils';
|
||||
import { Encrypter } from '@xocash/primitives';
|
||||
|
||||
// TODO: Only use pack/unpack on encrypted content.
|
||||
// Unsure why you were doing it on non-encrypted content (as it's not needed for IDB).
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { BaseStore } from './base-store.js';
|
||||
import { Encrypter, SSEClient } from '../utils';
|
||||
import { SSEClient } from '../utils';
|
||||
import {
|
||||
Bytes,
|
||||
Encrypter,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
decodeExtendedJson,
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
export * from './encrypter.js';
|
||||
export * from './event-emitter.js';
|
||||
export * from './ext-error.js';
|
||||
export * from './hasher.js';
|
||||
export * from './inverse-bloom-filter.js';
|
||||
export * from './memoizer.js';
|
||||
export * from './misc.js';
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
import { Encrypter } from '~/src/utils/encrypter';
|
||||
import { binToUtf8, utf8ToBin } from '@bitauth/libauth';
|
||||
|
||||
describe('Encrypter', () => {
|
||||
let encryption: Encrypter;
|
||||
const testKey = 'test-secret-key-123';
|
||||
|
||||
beforeEach(async () => {
|
||||
encryption = await Encrypter.fromPassphrase(testKey);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create instance with string key', async () => {
|
||||
const enc = await Encrypter.fromPassphrase('test-key');
|
||||
expect(enc).toBeInstanceOf(Encrypter);
|
||||
});
|
||||
|
||||
it('should create instance with Uint8Array key', async () => {
|
||||
const enc = await Encrypter.fromBytes(utf8ToBin('test-key'));
|
||||
expect(enc).toBeInstanceOf(Encrypter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypt', () => {
|
||||
it('should encrypt string data', async () => {
|
||||
const data = 'Hello, World!';
|
||||
const encrypted = await encryption.encrypt(data);
|
||||
expect(encrypted).toBeInstanceOf(Uint8Array);
|
||||
expect(encrypted.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should encrypt Uint8Array data', async () => {
|
||||
const data = utf8ToBin('Hello, World!');
|
||||
const encrypted = await encryption.encrypt(data);
|
||||
expect(encrypted).toBeInstanceOf(Uint8Array);
|
||||
expect(encrypted.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should produce different ciphertext for same input due to random IV', async () => {
|
||||
const data = 'Hello, World!';
|
||||
const encrypted1 = await encryption.encrypt(data);
|
||||
const encrypted2 = await encryption.encrypt(data);
|
||||
expect(Buffer.from(encrypted1).toString('hex')).not.toBe(
|
||||
Buffer.from(encrypted2).toString('hex'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decrypt', () => {
|
||||
it('should decrypt encrypted string data correctly', async () => {
|
||||
const originalData = 'Hello, World!';
|
||||
const encrypted = await encryption.encrypt(originalData);
|
||||
const decrypted = await encryption.decrypt(encrypted);
|
||||
expect(binToUtf8(decrypted)).toBe(originalData);
|
||||
});
|
||||
|
||||
it('should decrypt encrypted Uint8Array data correctly', async () => {
|
||||
const originalData = utf8ToBin('Hello, World!');
|
||||
const encrypted = await encryption.encrypt(originalData);
|
||||
const decrypted = await encryption.decrypt(encrypted);
|
||||
expect(decrypted).toEqual(originalData);
|
||||
});
|
||||
|
||||
it('should throw error when trying to decrypt string input', async () => {
|
||||
await expect(encryption.decrypt('invalid-input' as any)).rejects.toThrow(
|
||||
'Encrypted payload must be Uint8Array',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when trying to decrypt invalid data', async () => {
|
||||
const invalidData = new Uint8Array([1, 2, 3, 4]);
|
||||
await expect(encryption.decrypt(invalidData)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-instance encryption', () => {
|
||||
it('should decrypt data encrypted with different instance using same key', async () => {
|
||||
const enc1 = await Encrypter.fromPassphrase(testKey);
|
||||
const enc2 = await Encrypter.fromPassphrase(testKey);
|
||||
|
||||
const originalData = 'Hello, World!';
|
||||
const encrypted = await enc1.encrypt(originalData);
|
||||
const decrypted = await enc2.decrypt(encrypted);
|
||||
|
||||
expect(binToUtf8(decrypted)).toBe(originalData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('large data handling', () => {
|
||||
it('should handle large data correctly', async () => {
|
||||
const largeData = 'x'.repeat(1000000); // 1MB of data
|
||||
const encrypted = await encryption.encrypt(largeData);
|
||||
const decrypted = await encryption.decrypt(encrypted);
|
||||
expect(binToUtf8(decrypted)).toBe(largeData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
Reference in New Issue
Block a user