Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* Incremented the limits for various RPC calls to accommodate larger data sets ([#1621](https://github.com/0xMiden/miden-client/pull/1621)).
* [BREAKING] Introduced named storage slots, changed `FilesystemKeystore` to not be generic over RNG ([#1626](https://github.com/0xMiden/miden-client/pull/1626)).
* Added `submit_new_transaction_with_prover` to the Rust client and `submitNewTransactionWithProver` to the WebClient([#1622](https://github.com/0xMiden/miden-client/pull/1622)).
* [BREAKING] Added E2E encryption for private notes to the Rust client ([#1636](https://github.com/0xMiden/miden-client/pull/1636)).

## 0.12.5 (2025-12-01)

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions bin/integration-tests/src/tests/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::sync::Arc;

use anyhow::{Context, Result};
use miden_client::account::AccountStorageMode;
use miden_client::address::{Address, AddressInterface, RoutingParameters};
use miden_client::asset::FungibleAsset;
use miden_client::auth::{AuthSchemeId, RPO_FALCON_SCHEME_ID};
use miden_client::note::NoteType;
Expand Down Expand Up @@ -159,10 +158,16 @@ async fn run_flow(
.await
.context("failed to insert faucet in sender")?;

// Build recipient address
let recipient_address = Address::new(recipient_account.id())
.with_routing_parameters(RoutingParameters::new(AddressInterface::BasicWallet))
.context("failed to build recipient address")?;
// Get a recipient's address
let recipient_addresses = recipient
.test_store()
.get_addresses_by_account_id(recipient_account.id())
.await
.context("failed to get recipient addresses")?;
let recipient_address = recipient_addresses
.first()
.context("recipient should have a default address (with encryption key)")?
.clone();

// Ensure recipient has no input notes
recipient.sync_state().await.context("recipient initial sync")?;
Expand Down
5 changes: 5 additions & 0 deletions bin/miden-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,15 @@ impl Cli {
let keystore = FilesystemKeyStore::new(cli_config.secret_keys_directory.clone())
.map_err(CliError::KeyStore)?;

// Create encryption keystore (shares directory with auth keystore)
let encryption_keystore = FilesystemKeyStore::new(cli_config.secret_keys_directory.clone())
.map_err(CliError::KeyStore)?;

let mut builder = ClientBuilder::new()
.sqlite_store(cli_config.store_filepath.clone())
.grpc_client(&cli_config.rpc.endpoint.clone().into(), Some(cli_config.rpc.timeout_ms))
.authenticator(Arc::new(keystore.clone()))
.encryption_keystore(Arc::new(encryption_keystore))
.in_debug_mode(in_debug_mode)
.tx_graceful_blocks(Some(TX_GRACEFUL_BLOCK_DELTA));

Expand Down
3 changes: 3 additions & 0 deletions bin/miden-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1079,12 +1079,15 @@ async fn create_rust_client_with_store_path(

let keystore = FilesystemKeyStore::new(temp_dir())?;

let encryption_keystore = Arc::new(keystore.clone());

Ok((
TestClient::new(
Arc::new(GrpcClient::new(&endpoint, 10_000)),
rng,
store,
Some(std::sync::Arc::new(keystore.clone())),
Some(encryption_keystore),
ExecutionOptions::new(
Some(MAX_TX_EXECUTION_CYCLES),
MIN_TX_EXECUTION_CYCLES,
Expand Down
42 changes: 42 additions & 0 deletions crates/idxdb-store/src/encryption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::from_value;
use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen_futures::{JsFuture, js_sys};

// WEB ENCRYPTION KEYSTORE HELPER
// ================================================================================================

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EncryptionKeyIdxdbObject {
pub key: String,
}

#[wasm_bindgen(module = "/src/js/accounts.js")]
extern "C" {
#[wasm_bindgen(js_name = insertEncryptionKey)]
pub fn idxdb_insert_encryption_key(address_hash: String, key_hex: String) -> js_sys::Promise;

#[wasm_bindgen(js_name = getEncryptionKeyByAddressHash)]
pub fn idxdb_get_encryption_key(address_hash: String) -> js_sys::Promise;
}

pub async fn insert_encryption_key(address_hash: String, key_hex: String) -> Result<(), JsValue> {
let promise = idxdb_insert_encryption_key(address_hash, key_hex);
JsFuture::from(promise).await?;

Ok(())
}

pub async fn get_encryption_key(address_hash: String) -> Result<Option<String>, JsValue> {
let promise = idxdb_get_encryption_key(address_hash);
let js_key = JsFuture::from(promise).await?;

let encryption_key_idxdb: Option<EncryptionKeyIdxdbObject> =
from_value(js_key).map_err(|err| {
JsValue::from_str(&format!("Error: failed to deserialize encryption key: {err}"))
})?;

Ok(encryption_key_idxdb.map(|k| k.key))
}
28 changes: 27 additions & 1 deletion crates/idxdb-store/src/js/accounts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { accountCodes, accountStorages, accountAssets, accountAuths, accounts, addresses, foreignAccountCode, storageMapEntries, trackedAccounts, } from "./schema.js";
import { accountCodes, accountStorages, accountAssets, accountAuths, encryptionKeys, accounts, addresses, foreignAccountCode, storageMapEntries, trackedAccounts, } from "./schema.js";
import { logWebStoreError, uint8ArrayToBase64 } from "./utils.js";
// GET FUNCTIONS
export async function getAccountIds() {
Expand Down Expand Up @@ -422,3 +422,29 @@ export async function undoAccountStates(accountCommitments) {
logWebStoreError(error, `Error undoing account states: ${accountCommitments.join(",")}`);
}
}
// ENCRYPTION KEYS
// ================================================================================================
export async function insertEncryptionKey(addressHash, keyHex) {
try {
const data = {
addressHash: addressHash,
key: keyHex,
};
await encryptionKeys.put(data);
}
catch (error) {
logWebStoreError(error, `Error inserting encryption key for address hash: ${addressHash}`);
}
}
export async function getEncryptionKeyByAddressHash(addressHash) {
const encryptionKey = await encryptionKeys
.where("addressHash")
.equals(addressHash)
.first();
if (!encryptionKey) {
return null;
}
return {
key: encryptionKey.key,
};
}
5 changes: 4 additions & 1 deletion crates/idxdb-store/src/js/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var Table;
Table["AccountAssets"] = "accountAssets";
Table["StorageMapEntries"] = "storageMapEntries";
Table["AccountAuth"] = "accountAuth";
Table["EncryptionKeys"] = "encryptionKeys";
Table["Accounts"] = "accounts";
Table["Addresses"] = "addresses";
Table["Transactions"] = "transactions";
Expand All @@ -47,6 +48,7 @@ db.version(1).stores({
[Table.StorageMapEntries]: indexes("[root+key]", "root"),
[Table.AccountAssets]: indexes("[root+vaultKey]", "root", "faucetIdPrefix"),
[Table.AccountAuth]: indexes("pubKey"),
[Table.EncryptionKeys]: indexes("&addressHash"),
[Table.Accounts]: indexes("&accountCommitment", "id", "[id+nonce]", "codeRoot", "storageRoot", "vaultRoot"),
[Table.Addresses]: indexes("address", "id"),
[Table.Transactions]: indexes("id", "statusVariant"),
Expand Down Expand Up @@ -76,6 +78,7 @@ const accountStorages = db.table(Table.AccountStorage);
const storageMapEntries = db.table(Table.StorageMapEntries);
const accountAssets = db.table(Table.AccountAssets);
const accountAuths = db.table(Table.AccountAuth);
const encryptionKeys = db.table(Table.EncryptionKeys);
const accounts = db.table(Table.Accounts);
const addresses = db.table(Table.Addresses);
const transactions = db.table(Table.Transactions);
Expand Down Expand Up @@ -137,4 +140,4 @@ async function persistClientVersion(clientVersion) {
value: textEncoder.encode(clientVersion),
});
}
export { db, accountCodes, accountStorages, storageMapEntries, accountAssets, accountAuths, accounts, addresses, transactions, transactionScripts, inputNotes, outputNotes, notesScripts, stateSync, blockHeaders, partialBlockchainNodes, tags, foreignAccountCode, settings, trackedAccounts, };
export { db, accountCodes, accountStorages, storageMapEntries, accountAssets, accountAuths, encryptionKeys, accounts, addresses, transactions, transactionScripts, inputNotes, outputNotes, notesScripts, stateSync, blockHeaders, partialBlockchainNodes, tags, foreignAccountCode, settings, trackedAccounts, };
1 change: 1 addition & 0 deletions crates/idxdb-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use wasm_bindgen_futures::{JsFuture, js_sys};
pub mod account;
pub mod auth;
pub mod chain_data;
pub mod encryption;
pub mod export;
pub mod import;
pub mod note;
Expand Down
35 changes: 35 additions & 0 deletions crates/idxdb-store/src/ts/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
accountStorages,
accountAssets,
accountAuths,
encryptionKeys,
accounts,
addresses,
foreignAccountCode,
Expand Down Expand Up @@ -538,3 +539,37 @@ export async function undoAccountStates(accountCommitments: string[]) {
);
}
}

// ENCRYPTION KEYS
// ================================================================================================

export async function insertEncryptionKey(addressHash: string, keyHex: string) {
try {
const data = {
addressHash: addressHash,
key: keyHex,
};

await encryptionKeys.put(data);
} catch (error) {
logWebStoreError(
error,
`Error inserting encryption key for address hash: ${addressHash}`
);
}
}

export async function getEncryptionKeyByAddressHash(addressHash: string) {
const encryptionKey = await encryptionKeys
.where("addressHash")
.equals(addressHash)
.first();

if (!encryptionKey) {
return null;
}

return {
key: encryptionKey.key,
};
}
10 changes: 10 additions & 0 deletions crates/idxdb-store/src/ts/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum Table {
AccountAssets = "accountAssets",
StorageMapEntries = "storageMapEntries",
AccountAuth = "accountAuth",
EncryptionKeys = "encryptionKeys",
Accounts = "accounts",
Addresses = "addresses",
Transactions = "transactions",
Expand Down Expand Up @@ -72,6 +73,11 @@ export interface IAccountAuth {
secretKey: string;
}

export interface IEncryptionKey {
addressHash: string;
key: string;
}

export interface IAccount {
id: string;
codeRoot: string;
Expand Down Expand Up @@ -175,6 +181,7 @@ const db = new Dexie(DATABASE_NAME) as Dexie & {
accountAssets: Dexie.Table<IAccountAsset, string>;
storageMapEntries: Dexie.Table<IStorageMapEntry, string>;
accountAuths: Dexie.Table<IAccountAuth, string>;
encryptionKeys: Dexie.Table<IEncryptionKey, string>;
accounts: Dexie.Table<IAccount, string>;
addresses: Dexie.Table<IAddress, string>;
transactions: Dexie.Table<ITransaction, string>;
Expand All @@ -197,6 +204,7 @@ db.version(1).stores({
[Table.StorageMapEntries]: indexes("[root+key]", "root"),
[Table.AccountAssets]: indexes("[root+vaultKey]", "root", "faucetIdPrefix"),
[Table.AccountAuth]: indexes("pubKey"),
[Table.EncryptionKeys]: indexes("&addressHash"),
[Table.Accounts]: indexes(
"&accountCommitment",
"id",
Expand Down Expand Up @@ -243,6 +251,7 @@ const storageMapEntries = db.table<IStorageMapEntry, string>(
);
const accountAssets = db.table<IAccountAsset, string>(Table.AccountAssets);
const accountAuths = db.table<IAccountAuth, string>(Table.AccountAuth);
const encryptionKeys = db.table<IEncryptionKey, string>(Table.EncryptionKeys);
const accounts = db.table<IAccount, string>(Table.Accounts);
const addresses = db.table<IAddress, string>(Table.Addresses);
const transactions = db.table<ITransaction, string>(Table.Transactions);
Expand Down Expand Up @@ -333,6 +342,7 @@ export {
storageMapEntries,
accountAssets,
accountAuths,
encryptionKeys,
accounts,
addresses,
transactions,
Expand Down
38 changes: 37 additions & 1 deletion crates/rust-client/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ use alloc::vec::Vec;
use miden_lib::account::auth::{AuthEcdsaK256Keccak, AuthRpoFalcon512};
use miden_lib::account::wallets::BasicWallet;
use miden_objects::account::auth::PublicKey;
use miden_objects::address::RoutingParameters;
use miden_objects::crypto::dsa::eddsa_25519::SecretKey;
use miden_objects::crypto::ies::{SealingKey, UnsealingKey};
use miden_objects::note::NoteTag;
// RE-EXPORTS
// ================================================================================================
Expand Down Expand Up @@ -172,7 +175,40 @@ impl<AUTH> Client<AUTH> {

match tracked_account {
None => {
let default_address = Address::new(account.id());
// Generate encryption key pair and create default address with public key
let default_address = if let Some(ref keystore) = self.encryption_keystore {
// Generate encryption X25519 key pair
let mut rng = rand::rng();
let secret_key = SecretKey::with_rng(&mut rng);
let public_key = secret_key.public_key();

let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key);
let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key);

// Create address with encryption key
let address = Address::new(account.id())
.with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_encryption_key(sealing_key),
)
.map_err(|e| {
ClientError::ClientInitializationError(format!(
"Failed to create address with encryption key: {e}"
))
})?;

// Store key in keystore by address
keystore.add_encryption_key(&address, &unsealing_key).await.map_err(|e| {
ClientError::ClientInitializationError(format!(
"Failed to store encryption key: {e}"
))
})?;

address
} else {
// No keystore - use plain address
Address::new(account.id())
};

// If the account is not being tracked, insert it into the store regardless of the
// `overwrite` flag
Expand Down
Loading
Loading