privacycash 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +67 -0
- package/README.md +22 -0
- package/__tests__/e2e.test.ts +52 -0
- package/__tests__/encryption.test.ts +1635 -0
- package/circuit2/transaction2.wasm +0 -0
- package/circuit2/transaction2.zkey +0 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +16 -0
- package/dist/deposit.d.ts +18 -0
- package/dist/deposit.js +402 -0
- package/dist/exportUtils.d.ts +6 -0
- package/dist/exportUtils.js +6 -0
- package/dist/getUtxos.d.ts +27 -0
- package/dist/getUtxos.js +352 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +169 -0
- package/dist/models/keypair.d.ts +26 -0
- package/dist/models/keypair.js +43 -0
- package/dist/models/utxo.d.ts +49 -0
- package/dist/models/utxo.js +76 -0
- package/dist/utils/address_lookup_table.d.ts +8 -0
- package/dist/utils/address_lookup_table.js +21 -0
- package/dist/utils/constants.d.ts +14 -0
- package/dist/utils/constants.js +15 -0
- package/dist/utils/encryption.d.ts +107 -0
- package/dist/utils/encryption.js +374 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/merkle_tree.d.ts +92 -0
- package/dist/utils/merkle_tree.js +186 -0
- package/dist/utils/node-shim.d.ts +5 -0
- package/dist/utils/node-shim.js +5 -0
- package/dist/utils/prover.d.ts +33 -0
- package/dist/utils/prover.js +123 -0
- package/dist/utils/utils.d.ts +67 -0
- package/dist/utils/utils.js +151 -0
- package/dist/withdraw.d.ts +21 -0
- package/dist/withdraw.js +270 -0
- package/package.json +48 -0
- package/src/config.ts +28 -0
- package/src/deposit.ts +496 -0
- package/src/exportUtils.ts +6 -0
- package/src/getUtxos.ts +466 -0
- package/src/index.ts +191 -0
- package/src/models/keypair.ts +52 -0
- package/src/models/utxo.ts +97 -0
- package/src/utils/address_lookup_table.ts +29 -0
- package/src/utils/constants.ts +26 -0
- package/src/utils/encryption.ts +461 -0
- package/src/utils/logger.ts +42 -0
- package/src/utils/merkle_tree.ts +207 -0
- package/src/utils/node-shim.ts +6 -0
- package/src/utils/prover.ts +189 -0
- package/src/utils/utils.ts +213 -0
- package/src/withdraw.ts +334 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTXO (Unspent Transaction Output) module for ZK Cash
|
|
3
|
+
*
|
|
4
|
+
* Provides UTXO functionality for the ZK Cash system
|
|
5
|
+
* Based on: https://github.com/tornadocash/tornado-nova
|
|
6
|
+
*/
|
|
7
|
+
import BN from 'bn.js';
|
|
8
|
+
import { Keypair } from './keypair.js';
|
|
9
|
+
import * as hasher from '@lightprotocol/hasher.rs';
|
|
10
|
+
/**
|
|
11
|
+
* Simplified Utxo class inspired by Tornado Cash Nova
|
|
12
|
+
* Based on: https://github.com/tornadocash/tornado-nova/blob/f9264eeffe48bf5e04e19d8086ee6ec58cdf0d9e/src/utxo.js
|
|
13
|
+
*/
|
|
14
|
+
export declare class Utxo {
|
|
15
|
+
amount: BN;
|
|
16
|
+
blinding: BN;
|
|
17
|
+
keypair: Keypair;
|
|
18
|
+
index: number;
|
|
19
|
+
mintAddress: string;
|
|
20
|
+
version: 'v1' | 'v2';
|
|
21
|
+
private lightWasm;
|
|
22
|
+
constructor({ lightWasm, amount,
|
|
23
|
+
/**
|
|
24
|
+
* Tornado nova doesn't use solana eddsa with curve 25519 but their own "keypair"
|
|
25
|
+
* which is:
|
|
26
|
+
* - private key: random [31;u8]
|
|
27
|
+
* - public key: PoseidonHash(privateKey)
|
|
28
|
+
*
|
|
29
|
+
* Generate a new keypair for each UTXO
|
|
30
|
+
*/
|
|
31
|
+
keypair, blinding, // Use fixed value for consistency instead of randomBN()
|
|
32
|
+
index, mintAddress, // Default to Solana native SOL mint address,
|
|
33
|
+
version }: {
|
|
34
|
+
lightWasm: hasher.LightWasm;
|
|
35
|
+
amount?: BN | number | string;
|
|
36
|
+
keypair?: Keypair;
|
|
37
|
+
blinding?: BN | number | string;
|
|
38
|
+
index?: number;
|
|
39
|
+
mintAddress?: string;
|
|
40
|
+
version?: 'v1' | 'v2';
|
|
41
|
+
});
|
|
42
|
+
getCommitment(): Promise<string>;
|
|
43
|
+
getNullifier(): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Log all the UTXO's public properties and derived values in JSON format
|
|
46
|
+
* @returns Promise that resolves once all logging is complete
|
|
47
|
+
*/
|
|
48
|
+
log(): Promise<void>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTXO (Unspent Transaction Output) module for ZK Cash
|
|
3
|
+
*
|
|
4
|
+
* Provides UTXO functionality for the ZK Cash system
|
|
5
|
+
* Based on: https://github.com/tornadocash/tornado-nova
|
|
6
|
+
*/
|
|
7
|
+
import BN from 'bn.js';
|
|
8
|
+
import { Keypair } from './keypair.js';
|
|
9
|
+
import { ethers } from 'ethers';
|
|
10
|
+
/**
|
|
11
|
+
* Simplified Utxo class inspired by Tornado Cash Nova
|
|
12
|
+
* Based on: https://github.com/tornadocash/tornado-nova/blob/f9264eeffe48bf5e04e19d8086ee6ec58cdf0d9e/src/utxo.js
|
|
13
|
+
*/
|
|
14
|
+
export class Utxo {
|
|
15
|
+
amount;
|
|
16
|
+
blinding;
|
|
17
|
+
keypair;
|
|
18
|
+
index;
|
|
19
|
+
mintAddress;
|
|
20
|
+
version;
|
|
21
|
+
lightWasm;
|
|
22
|
+
constructor({ lightWasm, amount = new BN(0),
|
|
23
|
+
/**
|
|
24
|
+
* Tornado nova doesn't use solana eddsa with curve 25519 but their own "keypair"
|
|
25
|
+
* which is:
|
|
26
|
+
* - private key: random [31;u8]
|
|
27
|
+
* - public key: PoseidonHash(privateKey)
|
|
28
|
+
*
|
|
29
|
+
* Generate a new keypair for each UTXO
|
|
30
|
+
*/
|
|
31
|
+
keypair, blinding = new BN(Math.floor(Math.random() * 1000000000)), // Use fixed value for consistency instead of randomBN()
|
|
32
|
+
index = 0, mintAddress = '11111111111111111111111111111112', // Default to Solana native SOL mint address,
|
|
33
|
+
version = 'v2' }) {
|
|
34
|
+
this.amount = new BN(amount.toString());
|
|
35
|
+
this.blinding = new BN(blinding.toString());
|
|
36
|
+
this.lightWasm = lightWasm;
|
|
37
|
+
this.keypair = keypair || new Keypair(ethers.Wallet.createRandom().privateKey, lightWasm);
|
|
38
|
+
this.index = index;
|
|
39
|
+
this.mintAddress = mintAddress;
|
|
40
|
+
this.version = version;
|
|
41
|
+
}
|
|
42
|
+
async getCommitment() {
|
|
43
|
+
return this.lightWasm.poseidonHashString([this.amount.toString(), this.keypair.pubkey.toString(), this.blinding.toString(), this.mintAddress]);
|
|
44
|
+
}
|
|
45
|
+
async getNullifier() {
|
|
46
|
+
const commitmentValue = await this.getCommitment();
|
|
47
|
+
const signature = this.keypair.sign(commitmentValue, new BN(this.index).toString());
|
|
48
|
+
return this.lightWasm.poseidonHashString([commitmentValue, new BN(this.index).toString(), signature]);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Log all the UTXO's public properties and derived values in JSON format
|
|
52
|
+
* @returns Promise that resolves once all logging is complete
|
|
53
|
+
*/
|
|
54
|
+
async log() {
|
|
55
|
+
// Prepare the UTXO data object
|
|
56
|
+
const utxoData = {
|
|
57
|
+
amount: this.amount.toString(),
|
|
58
|
+
blinding: this.blinding.toString(),
|
|
59
|
+
index: this.index,
|
|
60
|
+
mintAddress: this.mintAddress,
|
|
61
|
+
keypair: {
|
|
62
|
+
pubkey: this.keypair.pubkey.toString()
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
// Add derived values
|
|
66
|
+
try {
|
|
67
|
+
utxoData.commitment = await this.getCommitment();
|
|
68
|
+
utxoData.nullifier = await this.getNullifier();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
utxoData.error = error.message;
|
|
72
|
+
}
|
|
73
|
+
// Output as formatted JSON
|
|
74
|
+
console.log(JSON.stringify(utxoData, null, 2));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Connection, PublicKey } from '@solana/web3.js';
|
|
2
|
+
/**
|
|
3
|
+
* Helper function to use an existing ALT (recommended for production)
|
|
4
|
+
* Use create_alt.ts to create the ALT once, then hardcode the address and use this function
|
|
5
|
+
*/
|
|
6
|
+
export declare function useExistingALT(connection: Connection, altAddress: PublicKey): Promise<{
|
|
7
|
+
value: any;
|
|
8
|
+
} | null>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper function to use an existing ALT (recommended for production)
|
|
3
|
+
* Use create_alt.ts to create the ALT once, then hardcode the address and use this function
|
|
4
|
+
*/
|
|
5
|
+
export async function useExistingALT(connection, altAddress) {
|
|
6
|
+
try {
|
|
7
|
+
console.log(`Using existing ALT: ${altAddress.toString()}`);
|
|
8
|
+
const altAccount = await connection.getAddressLookupTable(altAddress);
|
|
9
|
+
if (altAccount.value) {
|
|
10
|
+
console.log(`✅ ALT found with ${altAccount.value.state.addresses.length} addresses`);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
console.log('❌ ALT not found');
|
|
14
|
+
}
|
|
15
|
+
return altAccount;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error('Error getting existing ALT:', error);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PublicKey } from '@solana/web3.js';
|
|
2
|
+
import BN from 'bn.js';
|
|
3
|
+
export declare const FIELD_SIZE: BN;
|
|
4
|
+
export declare const PROGRAM_ID: PublicKey;
|
|
5
|
+
export declare const DEPLOYER_ID: PublicKey;
|
|
6
|
+
export declare const FEE_RECIPIENT: PublicKey;
|
|
7
|
+
export declare const FETCH_UTXOS_GROUP_SIZE = 2000;
|
|
8
|
+
export declare const TRANSACT_IX_DISCRIMINATOR: Buffer<ArrayBuffer>;
|
|
9
|
+
export declare const MERKLE_TREE_DEPTH = 26;
|
|
10
|
+
export declare const ALT_ADDRESS: PublicKey;
|
|
11
|
+
export declare const INDEXER_API_URL: string;
|
|
12
|
+
export declare const SIGN_MESSAGE = "Privacy Money account sign in";
|
|
13
|
+
export declare const LSK_FETCH_OFFSET = "fetch_offset";
|
|
14
|
+
export declare const LSK_ENCRYPTED_OUTPUTS = "encrypted_outputs";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PublicKey } from '@solana/web3.js';
|
|
2
|
+
import BN from 'bn.js';
|
|
3
|
+
export const FIELD_SIZE = new BN('21888242871839275222246405745257275088548364400416034343698204186575808495617');
|
|
4
|
+
export const PROGRAM_ID = new PublicKey('9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD');
|
|
5
|
+
export const DEPLOYER_ID = new PublicKey('AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM');
|
|
6
|
+
export const FEE_RECIPIENT = new PublicKey('AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM');
|
|
7
|
+
export const FETCH_UTXOS_GROUP_SIZE = 2000;
|
|
8
|
+
export const TRANSACT_IX_DISCRIMINATOR = Buffer.from([217, 149, 130, 143, 221, 52, 252, 119]);
|
|
9
|
+
export const MERKLE_TREE_DEPTH = 26;
|
|
10
|
+
export const ALT_ADDRESS = new PublicKey('72bpRay17JKp4k8H87p7ieU9C6aRDy5yCqwvtpTN2wuU');
|
|
11
|
+
export const INDEXER_API_URL = process.env.NEXT_PUBLIC_INDEXER_API_URL ?? 'https://api3.privacycash.org';
|
|
12
|
+
export const SIGN_MESSAGE = `Privacy Money account sign in`;
|
|
13
|
+
// localStorage cache keys
|
|
14
|
+
export const LSK_FETCH_OFFSET = 'fetch_offset';
|
|
15
|
+
export const LSK_ENCRYPTED_OUTPUTS = 'encrypted_outputs';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Keypair } from '@solana/web3.js';
|
|
2
|
+
import { Utxo } from '../models/utxo.js';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a UTXO with minimal required fields
|
|
5
|
+
*/
|
|
6
|
+
export interface UtxoData {
|
|
7
|
+
amount: string;
|
|
8
|
+
blinding: string;
|
|
9
|
+
index: number | string;
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
}
|
|
12
|
+
export interface EncryptionKey {
|
|
13
|
+
v1: Uint8Array;
|
|
14
|
+
v2: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Service for handling encryption and decryption of UTXO data
|
|
18
|
+
*/
|
|
19
|
+
export declare class EncryptionService {
|
|
20
|
+
static readonly ENCRYPTION_VERSION_V2: Buffer<ArrayBuffer>;
|
|
21
|
+
private encryptionKeyV1;
|
|
22
|
+
private encryptionKeyV2;
|
|
23
|
+
private utxoPrivateKeyV1;
|
|
24
|
+
private utxoPrivateKeyV2;
|
|
25
|
+
/**
|
|
26
|
+
* Generate an encryption key from a signature
|
|
27
|
+
* @param signature The user's signature
|
|
28
|
+
* @returns The generated encryption key
|
|
29
|
+
*/
|
|
30
|
+
deriveEncryptionKeyFromSignature(signature: Uint8Array): EncryptionKey;
|
|
31
|
+
/**
|
|
32
|
+
* Generate an encryption key from a wallet keypair (V2 format)
|
|
33
|
+
* @param keypair The Solana keypair to derive the encryption key from
|
|
34
|
+
* @returns The generated encryption key
|
|
35
|
+
*/
|
|
36
|
+
deriveEncryptionKeyFromWallet(keypair: Keypair): EncryptionKey;
|
|
37
|
+
/**
|
|
38
|
+
* Encrypt data with the stored encryption key
|
|
39
|
+
* @param data The data to encrypt
|
|
40
|
+
* @returns The encrypted data as a Buffer
|
|
41
|
+
* @throws Error if the encryption key has not been generated
|
|
42
|
+
*/
|
|
43
|
+
encrypt(data: Buffer | string): Buffer;
|
|
44
|
+
encryptDecryptedDoNotUse(data: Buffer | string): Buffer;
|
|
45
|
+
/**
|
|
46
|
+
* Decrypt data with the stored encryption key
|
|
47
|
+
* @param encryptedData The encrypted data to decrypt
|
|
48
|
+
* @returns The decrypted data as a Buffer
|
|
49
|
+
* @throws Error if the encryption key has not been generated or if the wrong key is used
|
|
50
|
+
*/
|
|
51
|
+
decrypt(encryptedData: Buffer): Buffer;
|
|
52
|
+
/**
|
|
53
|
+
* Decrypt data using the old V1 format (120-bit HMAC with SHA256)
|
|
54
|
+
* @param encryptedData The encrypted data to decrypt
|
|
55
|
+
* @param keypair Optional keypair to derive V1 key for backward compatibility
|
|
56
|
+
* @returns The decrypted data as a Buffer
|
|
57
|
+
*/
|
|
58
|
+
private decryptV1;
|
|
59
|
+
private timingSafeEqual;
|
|
60
|
+
/**
|
|
61
|
+
* Decrypt data using the new V2 format (256-bit Keccak HMAC)
|
|
62
|
+
* @param encryptedData The encrypted data to decrypt
|
|
63
|
+
* @returns The decrypted data as a Buffer
|
|
64
|
+
*/
|
|
65
|
+
private decryptV2;
|
|
66
|
+
/**
|
|
67
|
+
* Reset the encryption keys (mainly for testing purposes)
|
|
68
|
+
*/
|
|
69
|
+
resetEncryptionKey(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Encrypt a UTXO using a compact pipe-delimited format
|
|
72
|
+
* Always uses V2 encryption format. The UTXO's version property is used only for key derivation.
|
|
73
|
+
* @param utxo The UTXO to encrypt (includes version property)
|
|
74
|
+
* @returns The encrypted UTXO data as a Buffer
|
|
75
|
+
* @throws Error if the V2 encryption key has not been set
|
|
76
|
+
*/
|
|
77
|
+
encryptUtxo(utxo: Utxo): Buffer;
|
|
78
|
+
encryptUtxoDecryptedDoNotUse(utxo: Utxo): Buffer;
|
|
79
|
+
getEncryptionKeyVersion(encryptedData: Buffer | string): 'v1' | 'v2';
|
|
80
|
+
/**
|
|
81
|
+
* Decrypt an encrypted UTXO and parse it to a Utxo instance
|
|
82
|
+
* Automatically detects the UTXO version based on the encryption format
|
|
83
|
+
* @param encryptedData The encrypted UTXO data
|
|
84
|
+
* @param keypair The UTXO keypair to use for the decrypted UTXO
|
|
85
|
+
* @param lightWasm Optional LightWasm instance. If not provided, a new one will be created
|
|
86
|
+
* @param walletKeypair Optional wallet keypair for V1 backward compatibility
|
|
87
|
+
* @returns Promise resolving to the decrypted Utxo instance
|
|
88
|
+
* @throws Error if the encryption key has not been set or if decryption fails
|
|
89
|
+
*/
|
|
90
|
+
decryptUtxo(encryptedData: Buffer | string, lightWasm?: any): Promise<Utxo>;
|
|
91
|
+
getUtxoPrivateKeyWithVersion(version: 'v1' | 'v2'): string;
|
|
92
|
+
deriveUtxoPrivateKey(encryptedData?: Buffer | string): string;
|
|
93
|
+
hasUtxoPrivateKeyWithVersion(version: 'v1' | 'v2'): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Get the cached V1 UTXO private key
|
|
96
|
+
* @returns A private key in hex format that can be used to create a UTXO keypair
|
|
97
|
+
* @throws Error if V1 encryption key has not been set
|
|
98
|
+
*/
|
|
99
|
+
getUtxoPrivateKeyV1(): string;
|
|
100
|
+
/**
|
|
101
|
+
* Get the cached V2 UTXO private key
|
|
102
|
+
* @returns A private key in hex format that can be used to create a UTXO keypair
|
|
103
|
+
* @throws Error if V2 encryption key has not been set
|
|
104
|
+
*/
|
|
105
|
+
getUtxoPrivateKeyV2(): string;
|
|
106
|
+
}
|
|
107
|
+
export declare function serializeProofAndExtData(proof: any, extData: any): Buffer<ArrayBuffer>;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import nacl from 'tweetnacl';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import { Utxo } from '../models/utxo.js';
|
|
4
|
+
import { WasmFactory } from '@lightprotocol/hasher.rs';
|
|
5
|
+
import { Keypair as UtxoKeypair } from '../models/keypair.js';
|
|
6
|
+
import { keccak256 } from '@ethersproject/keccak256';
|
|
7
|
+
import { TRANSACT_IX_DISCRIMINATOR } from './constants.js';
|
|
8
|
+
import BN from 'bn.js';
|
|
9
|
+
/**
|
|
10
|
+
* Service for handling encryption and decryption of UTXO data
|
|
11
|
+
*/
|
|
12
|
+
export class EncryptionService {
|
|
13
|
+
static ENCRYPTION_VERSION_V2 = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]); // Version 2
|
|
14
|
+
encryptionKeyV1 = null;
|
|
15
|
+
encryptionKeyV2 = null;
|
|
16
|
+
utxoPrivateKeyV1 = null;
|
|
17
|
+
utxoPrivateKeyV2 = null;
|
|
18
|
+
/**
|
|
19
|
+
* Generate an encryption key from a signature
|
|
20
|
+
* @param signature The user's signature
|
|
21
|
+
* @returns The generated encryption key
|
|
22
|
+
*/
|
|
23
|
+
deriveEncryptionKeyFromSignature(signature) {
|
|
24
|
+
// Extract the first 31 bytes of the signature to create a deterministic key (legacy method)
|
|
25
|
+
const encryptionKeyV1 = signature.slice(0, 31);
|
|
26
|
+
// Store the V1 key in the service
|
|
27
|
+
this.encryptionKeyV1 = encryptionKeyV1;
|
|
28
|
+
// Precompute and cache the UTXO private key
|
|
29
|
+
const hashedSeedV1 = crypto.createHash('sha256').update(encryptionKeyV1).digest();
|
|
30
|
+
this.utxoPrivateKeyV1 = '0x' + hashedSeedV1.toString('hex');
|
|
31
|
+
// Use Keccak256 to derive a full 32-byte encryption key from the signature
|
|
32
|
+
const encryptionKeyV2 = Buffer.from(keccak256(signature).slice(2), 'hex');
|
|
33
|
+
// Store the V2 key in the service
|
|
34
|
+
this.encryptionKeyV2 = encryptionKeyV2;
|
|
35
|
+
// Precompute and cache the UTXO private key
|
|
36
|
+
const hashedSeedV2 = Buffer.from(keccak256(encryptionKeyV2).slice(2), 'hex');
|
|
37
|
+
this.utxoPrivateKeyV2 = '0x' + hashedSeedV2.toString('hex');
|
|
38
|
+
return {
|
|
39
|
+
v1: this.encryptionKeyV1,
|
|
40
|
+
v2: this.encryptionKeyV2
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generate an encryption key from a wallet keypair (V2 format)
|
|
45
|
+
* @param keypair The Solana keypair to derive the encryption key from
|
|
46
|
+
* @returns The generated encryption key
|
|
47
|
+
*/
|
|
48
|
+
deriveEncryptionKeyFromWallet(keypair) {
|
|
49
|
+
// Sign a constant message with the keypair
|
|
50
|
+
const message = Buffer.from('Privacy Money account sign in');
|
|
51
|
+
const signature = nacl.sign.detached(message, keypair.secretKey);
|
|
52
|
+
return this.deriveEncryptionKeyFromSignature(signature);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Encrypt data with the stored encryption key
|
|
56
|
+
* @param data The data to encrypt
|
|
57
|
+
* @returns The encrypted data as a Buffer
|
|
58
|
+
* @throws Error if the encryption key has not been generated
|
|
59
|
+
*/
|
|
60
|
+
encrypt(data) {
|
|
61
|
+
if (!this.encryptionKeyV2) {
|
|
62
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
63
|
+
}
|
|
64
|
+
// Convert string to Buffer if needed
|
|
65
|
+
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
66
|
+
// Generate a standard initialization vector (12 bytes for GCM)
|
|
67
|
+
const iv = crypto.randomBytes(12);
|
|
68
|
+
// Use the full 32-byte V2 encryption key for AES-256
|
|
69
|
+
const key = Buffer.from(this.encryptionKeyV2);
|
|
70
|
+
// Use AES-256-GCM for authenticated encryption
|
|
71
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
72
|
+
const encryptedData = Buffer.concat([
|
|
73
|
+
cipher.update(dataBuffer),
|
|
74
|
+
cipher.final()
|
|
75
|
+
]);
|
|
76
|
+
// Get the authentication tag from GCM (16 bytes)
|
|
77
|
+
const authTag = cipher.getAuthTag();
|
|
78
|
+
// Version 2 format: [version(8)] + [IV(12)] + [authTag(16)] + [encryptedData]
|
|
79
|
+
return Buffer.concat([
|
|
80
|
+
EncryptionService.ENCRYPTION_VERSION_V2,
|
|
81
|
+
iv,
|
|
82
|
+
authTag,
|
|
83
|
+
encryptedData
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
// v1 encryption, only used for testing now
|
|
87
|
+
encryptDecryptedDoNotUse(data) {
|
|
88
|
+
if (!this.encryptionKeyV1) {
|
|
89
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
90
|
+
}
|
|
91
|
+
// Convert string to Buffer if needed
|
|
92
|
+
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
93
|
+
// Generate a standard initialization vector (16 bytes)
|
|
94
|
+
const iv = crypto.randomBytes(16);
|
|
95
|
+
// Create a key from our encryption key (using only first 16 bytes for AES-128)
|
|
96
|
+
const key = Buffer.from(this.encryptionKeyV1).slice(0, 16);
|
|
97
|
+
// Use a more compact encryption algorithm (aes-128-ctr)
|
|
98
|
+
const cipher = crypto.createCipheriv('aes-128-ctr', key, iv);
|
|
99
|
+
const encryptedData = Buffer.concat([
|
|
100
|
+
cipher.update(dataBuffer),
|
|
101
|
+
cipher.final()
|
|
102
|
+
]);
|
|
103
|
+
// Create an authentication tag (HMAC) to verify decryption with correct key
|
|
104
|
+
const hmacKey = Buffer.from(this.encryptionKeyV1).slice(16, 31);
|
|
105
|
+
const hmac = crypto.createHmac('sha256', hmacKey);
|
|
106
|
+
hmac.update(iv);
|
|
107
|
+
hmac.update(encryptedData);
|
|
108
|
+
const authTag = hmac.digest().slice(0, 16); // Use first 16 bytes of HMAC as auth tag
|
|
109
|
+
// Combine IV, auth tag and encrypted data
|
|
110
|
+
return Buffer.concat([iv, authTag, encryptedData]);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Decrypt data with the stored encryption key
|
|
114
|
+
* @param encryptedData The encrypted data to decrypt
|
|
115
|
+
* @returns The decrypted data as a Buffer
|
|
116
|
+
* @throws Error if the encryption key has not been generated or if the wrong key is used
|
|
117
|
+
*/
|
|
118
|
+
decrypt(encryptedData) {
|
|
119
|
+
// Check if this is the new version format (starts with 8-byte version identifier)
|
|
120
|
+
if (encryptedData.length >= 8 && encryptedData.subarray(0, 8).equals(EncryptionService.ENCRYPTION_VERSION_V2)) {
|
|
121
|
+
if (!this.encryptionKeyV2) {
|
|
122
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
123
|
+
}
|
|
124
|
+
return this.decryptV2(encryptedData);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// V1 format - need V1 key or keypair to derive it
|
|
128
|
+
if (!this.encryptionKeyV1) {
|
|
129
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
130
|
+
}
|
|
131
|
+
return this.decryptV1(encryptedData);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Decrypt data using the old V1 format (120-bit HMAC with SHA256)
|
|
136
|
+
* @param encryptedData The encrypted data to decrypt
|
|
137
|
+
* @param keypair Optional keypair to derive V1 key for backward compatibility
|
|
138
|
+
* @returns The decrypted data as a Buffer
|
|
139
|
+
*/
|
|
140
|
+
decryptV1(encryptedData) {
|
|
141
|
+
if (!this.encryptionKeyV1) {
|
|
142
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
143
|
+
}
|
|
144
|
+
// Extract the IV from the first 16 bytes
|
|
145
|
+
const iv = encryptedData.slice(0, 16);
|
|
146
|
+
// Extract the auth tag from the next 16 bytes
|
|
147
|
+
const authTag = encryptedData.slice(16, 32);
|
|
148
|
+
// The rest is the actual encrypted data
|
|
149
|
+
const data = encryptedData.slice(32);
|
|
150
|
+
// Verify the authentication tag
|
|
151
|
+
const hmacKey = Buffer.from(this.encryptionKeyV1).slice(16, 31);
|
|
152
|
+
const hmac = crypto.createHmac('sha256', hmacKey);
|
|
153
|
+
hmac.update(iv);
|
|
154
|
+
hmac.update(data);
|
|
155
|
+
const calculatedTag = hmac.digest().slice(0, 16);
|
|
156
|
+
// Compare tags - if they don't match, the key is wrong
|
|
157
|
+
if (!this.timingSafeEqual(authTag, calculatedTag)) {
|
|
158
|
+
throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
|
|
159
|
+
}
|
|
160
|
+
// Create a key from our encryption key (using only first 16 bytes for AES-128)
|
|
161
|
+
const key = Buffer.from(this.encryptionKeyV1).slice(0, 16);
|
|
162
|
+
// Use the same algorithm as in encrypt
|
|
163
|
+
const decipher = crypto.createDecipheriv('aes-128-ctr', key, iv);
|
|
164
|
+
try {
|
|
165
|
+
return Buffer.concat([
|
|
166
|
+
decipher.update(data),
|
|
167
|
+
decipher.final()
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Custom timingSafeEqual for browser compatibility
|
|
175
|
+
timingSafeEqual(a, b) {
|
|
176
|
+
if (a.length !== b.length) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
let diff = 0;
|
|
180
|
+
for (let i = 0; i < a.length; i++) {
|
|
181
|
+
diff |= a[i] ^ b[i];
|
|
182
|
+
}
|
|
183
|
+
return diff === 0;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Decrypt data using the new V2 format (256-bit Keccak HMAC)
|
|
187
|
+
* @param encryptedData The encrypted data to decrypt
|
|
188
|
+
* @returns The decrypted data as a Buffer
|
|
189
|
+
*/
|
|
190
|
+
decryptV2(encryptedData) {
|
|
191
|
+
if (!this.encryptionKeyV2) {
|
|
192
|
+
throw new Error('encryptionKeyV2 not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
193
|
+
}
|
|
194
|
+
// Skip 8-byte version identifier and extract components for GCM format
|
|
195
|
+
const iv = encryptedData.slice(8, 20); // bytes 8-19 (12 bytes for GCM)
|
|
196
|
+
const authTag = encryptedData.slice(20, 36); // bytes 20-35 (16 bytes for GCM)
|
|
197
|
+
const data = encryptedData.slice(36); // remaining bytes
|
|
198
|
+
// Use the full 32-byte V2 encryption key for AES-256
|
|
199
|
+
const key = Buffer.from(this.encryptionKeyV2);
|
|
200
|
+
// Use AES-256-GCM for authenticated decryption
|
|
201
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
202
|
+
decipher.setAuthTag(authTag);
|
|
203
|
+
try {
|
|
204
|
+
return Buffer.concat([
|
|
205
|
+
decipher.update(data),
|
|
206
|
+
decipher.final()
|
|
207
|
+
]);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Reset the encryption keys (mainly for testing purposes)
|
|
215
|
+
*/
|
|
216
|
+
resetEncryptionKey() {
|
|
217
|
+
this.encryptionKeyV1 = null;
|
|
218
|
+
this.encryptionKeyV2 = null;
|
|
219
|
+
this.utxoPrivateKeyV1 = null;
|
|
220
|
+
this.utxoPrivateKeyV2 = null;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Encrypt a UTXO using a compact pipe-delimited format
|
|
224
|
+
* Always uses V2 encryption format. The UTXO's version property is used only for key derivation.
|
|
225
|
+
* @param utxo The UTXO to encrypt (includes version property)
|
|
226
|
+
* @returns The encrypted UTXO data as a Buffer
|
|
227
|
+
* @throws Error if the V2 encryption key has not been set
|
|
228
|
+
*/
|
|
229
|
+
encryptUtxo(utxo) {
|
|
230
|
+
if (!this.encryptionKeyV2) {
|
|
231
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
232
|
+
}
|
|
233
|
+
// Create a compact string representation using pipe delimiter
|
|
234
|
+
// Version is stored in the UTXO model, not in the encrypted content
|
|
235
|
+
const utxoString = `${utxo.amount.toString()}|${utxo.blinding.toString()}|${utxo.index}|${utxo.mintAddress}`;
|
|
236
|
+
// Always use V2 encryption format (which adds version byte 0x02 at the beginning)
|
|
237
|
+
return this.encrypt(utxoString);
|
|
238
|
+
}
|
|
239
|
+
// Deprecated, only used for testing now
|
|
240
|
+
encryptUtxoDecryptedDoNotUse(utxo) {
|
|
241
|
+
if (!this.encryptionKeyV2) {
|
|
242
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
243
|
+
}
|
|
244
|
+
const utxoString = `${utxo.amount.toString()}|${utxo.blinding.toString()}|${utxo.index}|${utxo.mintAddress}`;
|
|
245
|
+
return this.encryptDecryptedDoNotUse(utxoString);
|
|
246
|
+
}
|
|
247
|
+
getEncryptionKeyVersion(encryptedData) {
|
|
248
|
+
const buffer = typeof encryptedData === 'string' ? Buffer.from(encryptedData, 'hex') : encryptedData;
|
|
249
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(EncryptionService.ENCRYPTION_VERSION_V2)) {
|
|
250
|
+
// V2 encryption format → V2 UTXO
|
|
251
|
+
return 'v2';
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// V1 encryption format → UTXO
|
|
255
|
+
return 'v1';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Decrypt an encrypted UTXO and parse it to a Utxo instance
|
|
260
|
+
* Automatically detects the UTXO version based on the encryption format
|
|
261
|
+
* @param encryptedData The encrypted UTXO data
|
|
262
|
+
* @param keypair The UTXO keypair to use for the decrypted UTXO
|
|
263
|
+
* @param lightWasm Optional LightWasm instance. If not provided, a new one will be created
|
|
264
|
+
* @param walletKeypair Optional wallet keypair for V1 backward compatibility
|
|
265
|
+
* @returns Promise resolving to the decrypted Utxo instance
|
|
266
|
+
* @throws Error if the encryption key has not been set or if decryption fails
|
|
267
|
+
*/
|
|
268
|
+
async decryptUtxo(encryptedData, lightWasm) {
|
|
269
|
+
// Convert hex string to Buffer if needed
|
|
270
|
+
const encryptedBuffer = typeof encryptedData === 'string'
|
|
271
|
+
? Buffer.from(encryptedData, 'hex')
|
|
272
|
+
: encryptedData;
|
|
273
|
+
// Detect UTXO version based on encryption format
|
|
274
|
+
let utxoVersion = this.getEncryptionKeyVersion(encryptedBuffer);
|
|
275
|
+
// The decrypt() method already handles encryption format version detection (V1 vs V2)
|
|
276
|
+
// It checks the first byte to determine whether to use decryptV1() or decryptV2()
|
|
277
|
+
const decrypted = this.decrypt(encryptedBuffer);
|
|
278
|
+
// Parse the pipe-delimited format: amount|blinding|index|mintAddress
|
|
279
|
+
const decryptedStr = decrypted.toString();
|
|
280
|
+
const parts = decryptedStr.split('|');
|
|
281
|
+
if (parts.length !== 4) {
|
|
282
|
+
throw new Error('Invalid UTXO format after decryption');
|
|
283
|
+
}
|
|
284
|
+
const [amount, blinding, index, mintAddress] = parts;
|
|
285
|
+
if (!amount || !blinding || index === undefined || mintAddress === undefined) {
|
|
286
|
+
throw new Error('Invalid UTXO format after decryption');
|
|
287
|
+
}
|
|
288
|
+
// Get or create a LightWasm instance
|
|
289
|
+
const wasmInstance = lightWasm || await WasmFactory.getInstance();
|
|
290
|
+
const privateKey = this.getUtxoPrivateKeyWithVersion(utxoVersion);
|
|
291
|
+
// Create a Utxo instance with the detected version
|
|
292
|
+
const utxo = new Utxo({
|
|
293
|
+
lightWasm: wasmInstance,
|
|
294
|
+
amount: amount,
|
|
295
|
+
blinding: blinding,
|
|
296
|
+
keypair: new UtxoKeypair(privateKey, wasmInstance),
|
|
297
|
+
index: Number(index),
|
|
298
|
+
mintAddress: mintAddress,
|
|
299
|
+
version: utxoVersion
|
|
300
|
+
});
|
|
301
|
+
return utxo;
|
|
302
|
+
}
|
|
303
|
+
getUtxoPrivateKeyWithVersion(version) {
|
|
304
|
+
if (version === 'v1') {
|
|
305
|
+
return this.getUtxoPrivateKeyV1();
|
|
306
|
+
}
|
|
307
|
+
return this.getUtxoPrivateKeyV2();
|
|
308
|
+
}
|
|
309
|
+
deriveUtxoPrivateKey(encryptedData) {
|
|
310
|
+
if (encryptedData && this.getEncryptionKeyVersion(encryptedData) === 'v2') {
|
|
311
|
+
return this.getUtxoPrivateKeyWithVersion('v2');
|
|
312
|
+
}
|
|
313
|
+
return this.getUtxoPrivateKeyWithVersion('v1');
|
|
314
|
+
}
|
|
315
|
+
hasUtxoPrivateKeyWithVersion(version) {
|
|
316
|
+
if (version === 'v1') {
|
|
317
|
+
return !!this.utxoPrivateKeyV1;
|
|
318
|
+
}
|
|
319
|
+
return !!this.utxoPrivateKeyV2;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get the cached V1 UTXO private key
|
|
323
|
+
* @returns A private key in hex format that can be used to create a UTXO keypair
|
|
324
|
+
* @throws Error if V1 encryption key has not been set
|
|
325
|
+
*/
|
|
326
|
+
getUtxoPrivateKeyV1() {
|
|
327
|
+
if (!this.utxoPrivateKeyV1) {
|
|
328
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
329
|
+
}
|
|
330
|
+
return this.utxoPrivateKeyV1;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Get the cached V2 UTXO private key
|
|
334
|
+
* @returns A private key in hex format that can be used to create a UTXO keypair
|
|
335
|
+
* @throws Error if V2 encryption key has not been set
|
|
336
|
+
*/
|
|
337
|
+
getUtxoPrivateKeyV2() {
|
|
338
|
+
if (!this.utxoPrivateKeyV2) {
|
|
339
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
340
|
+
}
|
|
341
|
+
return this.utxoPrivateKeyV2;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
export function serializeProofAndExtData(proof, extData) {
|
|
345
|
+
// Create the ExtDataMinified object for the program call (only extAmount and fee)
|
|
346
|
+
const extDataMinified = {
|
|
347
|
+
extAmount: extData.extAmount,
|
|
348
|
+
fee: extData.fee
|
|
349
|
+
};
|
|
350
|
+
// Use the same serialization approach as deposit script
|
|
351
|
+
const instructionData = Buffer.concat([
|
|
352
|
+
TRANSACT_IX_DISCRIMINATOR,
|
|
353
|
+
// Serialize proof
|
|
354
|
+
Buffer.from(proof.proofA),
|
|
355
|
+
Buffer.from(proof.proofB),
|
|
356
|
+
Buffer.from(proof.proofC),
|
|
357
|
+
Buffer.from(proof.root),
|
|
358
|
+
Buffer.from(proof.publicAmount),
|
|
359
|
+
Buffer.from(proof.extDataHash),
|
|
360
|
+
Buffer.from(proof.inputNullifiers[0]),
|
|
361
|
+
Buffer.from(proof.inputNullifiers[1]),
|
|
362
|
+
Buffer.from(proof.outputCommitments[0]),
|
|
363
|
+
Buffer.from(proof.outputCommitments[1]),
|
|
364
|
+
// Serialize ExtDataMinified (only extAmount and fee)
|
|
365
|
+
Buffer.from(new BN(extDataMinified.extAmount).toTwos(64).toArray('le', 8)),
|
|
366
|
+
Buffer.from(new BN(extDataMinified.fee).toArray('le', 8)),
|
|
367
|
+
// Serialize encrypted outputs as separate parameters
|
|
368
|
+
Buffer.from(new BN(extData.encryptedOutput1.length).toArray('le', 4)),
|
|
369
|
+
extData.encryptedOutput1,
|
|
370
|
+
Buffer.from(new BN(extData.encryptedOutput2.length).toArray('le', 4)),
|
|
371
|
+
extData.encryptedOutput2,
|
|
372
|
+
]);
|
|
373
|
+
return instructionData;
|
|
374
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export type LoggerFn = (level: LogLevel, message: string) => void;
|
|
3
|
+
export declare function setLogger(logger: LoggerFn): void;
|
|
4
|
+
export declare const logger: {
|
|
5
|
+
debug: (...args: unknown[]) => void;
|
|
6
|
+
info: (...args: unknown[]) => void;
|
|
7
|
+
warn: (...args: unknown[]) => void;
|
|
8
|
+
error: (...args: unknown[]) => void;
|
|
9
|
+
};
|