mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 14:02:19 +00:00
183 lines
6.4 KiB
TypeScript
183 lines
6.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
deriveAesKeyFromMnemonic,
|
|
deriveBtcAddressFromMnemonic,
|
|
deriveEvmAddressFromMnemonic,
|
|
deriveSolanaAddressFromMnemonic,
|
|
deriveTronAddressFromMnemonic,
|
|
deriveWalletAccountsFromMnemonic,
|
|
generateMnemonicPhrase,
|
|
MNEMONIC_GENERATE_WORD_COUNT,
|
|
validateMnemonicPhrase,
|
|
} from './cryptoKeys';
|
|
|
|
// Known-good 12-word BIP39 mnemonic for deterministic assertions.
|
|
const KNOWN_MNEMONIC =
|
|
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
|
|
|
describe('MNEMONIC_GENERATE_WORD_COUNT', () => {
|
|
it('is 12', () => {
|
|
expect(MNEMONIC_GENERATE_WORD_COUNT).toBe(12);
|
|
});
|
|
});
|
|
|
|
describe('generateMnemonicPhrase', () => {
|
|
it('returns a 12-word phrase', () => {
|
|
const phrase = generateMnemonicPhrase();
|
|
expect(phrase.trim().split(/\s+/)).toHaveLength(12);
|
|
});
|
|
|
|
it('returns a valid BIP39 mnemonic', () => {
|
|
const phrase = generateMnemonicPhrase();
|
|
expect(validateMnemonicPhrase(phrase)).toBe(true);
|
|
});
|
|
|
|
it('produces a different phrase each call', () => {
|
|
const a = generateMnemonicPhrase();
|
|
const b = generateMnemonicPhrase();
|
|
// Astronomically unlikely to collide; guards against a no-op implementation.
|
|
expect(a).not.toBe(b);
|
|
});
|
|
});
|
|
|
|
describe('validateMnemonicPhrase', () => {
|
|
it('returns true for the known valid mnemonic', () => {
|
|
expect(validateMnemonicPhrase(KNOWN_MNEMONIC)).toBe(true);
|
|
});
|
|
|
|
it('returns false for an empty string', () => {
|
|
expect(validateMnemonicPhrase('')).toBe(false);
|
|
});
|
|
|
|
it('returns false for a single random word', () => {
|
|
expect(validateMnemonicPhrase('abandon')).toBe(false);
|
|
});
|
|
|
|
it('returns false for 12 non-BIP39 words', () => {
|
|
expect(
|
|
validateMnemonicPhrase('foo bar baz qux quux corge grault garply waldo fred plugh xyzzy')
|
|
).toBe(false);
|
|
});
|
|
|
|
it('returns false for an otherwise valid phrase with one word replaced by a non-wordlist word', () => {
|
|
const tampered = KNOWN_MNEMONIC.replace('abandon', 'xxxxxxxx');
|
|
expect(validateMnemonicPhrase(tampered)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('deriveAesKeyFromMnemonic', () => {
|
|
it('returns a 64-character hex string (256-bit key)', () => {
|
|
const key = deriveAesKeyFromMnemonic(KNOWN_MNEMONIC);
|
|
expect(key).toMatch(/^[0-9a-f]{64}$/);
|
|
});
|
|
|
|
it('is deterministic for the same mnemonic', () => {
|
|
const key1 = deriveAesKeyFromMnemonic(KNOWN_MNEMONIC);
|
|
const key2 = deriveAesKeyFromMnemonic(KNOWN_MNEMONIC);
|
|
expect(key1).toBe(key2);
|
|
});
|
|
|
|
it('produces a different key for a different mnemonic', () => {
|
|
const other = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong';
|
|
expect(validateMnemonicPhrase(other)).toBe(true);
|
|
const keyA = deriveAesKeyFromMnemonic(KNOWN_MNEMONIC);
|
|
const keyB = deriveAesKeyFromMnemonic(other);
|
|
expect(keyA).not.toBe(keyB);
|
|
});
|
|
|
|
it('returns a fixed known value for the all-abandon mnemonic', () => {
|
|
// Pinned output for salt='openhuman-aes-key-v1', PBKDF2-SHA256, c=100000, dkLen=32.
|
|
// If this assertion fails, the KDF parameters or salt have changed — update intentionally.
|
|
const key = deriveAesKeyFromMnemonic(KNOWN_MNEMONIC);
|
|
expect(key).toBe('dce707ee483afb0a70cb2e076295f9f914e0c62cc097895eabda1c0c1f2f0cb1');
|
|
});
|
|
});
|
|
|
|
describe('deriveEvmAddressFromMnemonic', () => {
|
|
it('returns a 0x-prefixed 42-character string', () => {
|
|
const address = deriveEvmAddressFromMnemonic(KNOWN_MNEMONIC);
|
|
expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
});
|
|
|
|
it('is deterministic for the same mnemonic', () => {
|
|
const addr1 = deriveEvmAddressFromMnemonic(KNOWN_MNEMONIC);
|
|
const addr2 = deriveEvmAddressFromMnemonic(KNOWN_MNEMONIC);
|
|
expect(addr1).toBe(addr2);
|
|
});
|
|
|
|
it('returns the well-known address for the all-abandon mnemonic', () => {
|
|
// MetaMask / BIP44 m/44'/60'/0'/0/0 for all-abandon is a stable known address.
|
|
// Pinned in EIP-55 checksummed form — validates both identity and checksum casing.
|
|
const address = deriveEvmAddressFromMnemonic(KNOWN_MNEMONIC);
|
|
expect(address).toBe('0x9858EfFD232B4033E47d90003D41EC34EcaEda94');
|
|
});
|
|
|
|
it('returns a different address for a different mnemonic', () => {
|
|
const other = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong';
|
|
const addrA = deriveEvmAddressFromMnemonic(KNOWN_MNEMONIC);
|
|
const addrB = deriveEvmAddressFromMnemonic(other);
|
|
expect(addrA).not.toBe(addrB);
|
|
});
|
|
|
|
it('produces a checksummed address (EIP-55)', () => {
|
|
const address = deriveEvmAddressFromMnemonic(KNOWN_MNEMONIC);
|
|
// At least some characters must be uppercase (otherwise it would just be lowercase hex).
|
|
// EIP-55 checksummed addresses contain mixed case by design.
|
|
const hex = address.slice(2);
|
|
// Not all-lowercase and not all-uppercase → mixed casing applied.
|
|
const hasUpper = hex !== hex.toLowerCase();
|
|
const hasLower = hex !== hex.toUpperCase();
|
|
expect(hasUpper && hasLower).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('deriveBtcAddressFromMnemonic', () => {
|
|
it('returns the well-known address for the all-abandon mnemonic', () => {
|
|
expect(deriveBtcAddressFromMnemonic(KNOWN_MNEMONIC)).toBe('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA');
|
|
});
|
|
});
|
|
|
|
describe('deriveSolanaAddressFromMnemonic', () => {
|
|
it('returns the well-known address for the all-abandon mnemonic', () => {
|
|
expect(deriveSolanaAddressFromMnemonic(KNOWN_MNEMONIC)).toBe(
|
|
'HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('deriveTronAddressFromMnemonic', () => {
|
|
it('returns the well-known address for the all-abandon mnemonic', () => {
|
|
expect(deriveTronAddressFromMnemonic(KNOWN_MNEMONIC)).toBe(
|
|
'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('deriveWalletAccountsFromMnemonic', () => {
|
|
it('returns one derived identity per supported chain in a stable order', () => {
|
|
expect(deriveWalletAccountsFromMnemonic(KNOWN_MNEMONIC)).toEqual([
|
|
{
|
|
chain: 'evm',
|
|
address: '0x9858EfFD232B4033E47d90003D41EC34EcaEda94',
|
|
derivationPath: "m/44'/60'/0'/0/0",
|
|
},
|
|
{
|
|
chain: 'btc',
|
|
address: '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA',
|
|
derivationPath: "m/44'/0'/0'/0/0",
|
|
},
|
|
{
|
|
chain: 'solana',
|
|
address: 'HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk',
|
|
derivationPath: "m/44'/501'/0'/0'",
|
|
},
|
|
{
|
|
chain: 'tron',
|
|
address: 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
|
derivationPath: "m/44'/195'/0'/0/0",
|
|
},
|
|
]);
|
|
});
|
|
});
|