Skip to content

Commit 1d89c4c

Browse files
committed
add import and change move encryption into export
1 parent ef63ba4 commit 1d89c4c

8 files changed

Lines changed: 285 additions & 51 deletions

File tree

Cargo.lock

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ solana-system-interface = { version = "2.0.0", features = ["bincode"] }
5454
bincode = "1.3.3"
5555
solana-transaction = { version = "3.0.1", features = ["bincode", "serde"] }
5656
mark-flaky-tests = { version = "1.0.2", features = ["tokio"] }
57+
secp256k1 = { version = "0.31.1", features = ["global-context", "rand"] }
58+
sha3 = "0.10.8"
5759

5860
[[example]]
5961
doc-scrape-examples = true

examples/wallet_export.rs

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@
2121
2222
use anyhow::Result;
2323
use hex::ToHex;
24-
use privy_rust::{
25-
AuthorizationContext, JwtUser, PrivateKey, PrivyClient,
26-
generated::types::{HpkeEncryption, WalletExportRequestBody},
27-
};
24+
use privy_rust::{AuthorizationContext, JwtUser, PrivateKey, PrivyClient};
2825
use tracing_subscriber::EnvFilter;
2926

3027
#[tokio::main]
@@ -49,10 +46,6 @@ async fn main() -> Result<()> {
4946
wallet_id
5047
);
5148

52-
// Generate HPKE key pair for encryption
53-
let hpke_keypair = privy_rust::privy_hpke::PrivyHpke::new();
54-
let recipient_public_key = hpke_keypair.public_key()?;
55-
5649
tracing::info!("Generated HPKE key pair for encryption");
5750

5851
let private_key = std::fs::read_to_string("private_key.pem")?;
@@ -64,31 +57,13 @@ async fn main() -> Result<()> {
6457
ctx.push(JwtUser(client.clone(), "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGV4QGFybHlvbi5kZXYiLCJpYXQiOjEwMDAwMDAwMDAwMH0.IpNgavH95CFZPjkzQW4eyxMIfJ-O_5cIaDyu_6KRXffykjYDRwxTgFJuYq0F6d8wSXf4de-vzfBRWSKMISM3rJdlhximYINGJB14mJFCD87VMLFbTpHIXcv7hc1AAYMPGhOsRkYfYXuvVopKszMvhupmQYJ1npSvKWNeBniIyOHYv4xebZD8L0RVlPvuEKTXTu-CDfs2rMwvD9g_wiBznS3uMF3v_KPaY6x0sx9zeCSxAH9zvhMMtct_Ad9kuoUncGpRzNhEk6JlVccN2Leb1JzbldxSywyS2AApD05u-GFAgFDN3P39V3qgRTGDuuUfUvKQ9S4rbu5El9Qq1CJTeA".to_string()));
6558

6659
// Export wallet private key (requires authorization signature)
67-
let export_response = client
68-
.wallets()
69-
.export(
70-
&wallet_id,
71-
&ctx,
72-
&WalletExportRequestBody {
73-
encryption_type: HpkeEncryption::Hpke,
74-
recipient_public_key,
75-
},
76-
)
77-
.await?;
78-
79-
tracing::info!("Received encrypted wallet export response");
80-
81-
// Decrypt the exported private key
82-
let decrypted_key = hpke_keypair.decrypt(
83-
&export_response.encapsulated_key,
84-
&export_response.ciphertext,
85-
)?;
60+
let secret_key = client.wallets().export(&wallet_id, &ctx).await?;
8661

8762
tracing::info!("Successfully decrypted private key");
8863
tracing::warn!("SECURITY WARNING: Private key exported and decrypted!");
8964
println!(
9065
"Decrypted private key (hex): {}",
91-
decrypted_key.to_bytes().encode_hex::<String>()
66+
secret_key.to_bytes().encode_hex::<String>()
9267
);
9368

9469
Ok(())

src/ethereum.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ impl EthereumService {
511511
/// gas_limit: None,
512512
/// max_fee_per_gas: None,
513513
/// max_priority_fee_per_gas: None,
514-
/// data: None,
514+
/// data: Some("0x".to_string()),
515515
/// chain_id: None,
516516
/// from: None,
517517
/// gas_price: None,

src/import.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use base64::Engine;
2+
use hpke::{
3+
Deserializable, OpModeS, Serializable, aead::ChaCha20Poly1305, kdf::HkdfSha256,
4+
kem::DhP256HkdfSha256,
5+
};
6+
use progenitor_client::{Error, ResponseValue};
7+
8+
use crate::{
9+
generated::types::{
10+
PrivateKeySubmitInput, Wallet, WalletImportInitializationResponse,
11+
WalletImportSubmissionRequest, WalletImportSubmissionRequestOwner,
12+
WalletImportSubmissionRequestWallet, WalletImportSupportedChains,
13+
},
14+
subclients::WalletsClient,
15+
};
16+
17+
pub struct WalletImport {
18+
client: WalletsClient,
19+
initialization_response: WalletImportInitializationResponse,
20+
address: String,
21+
chain_type: WalletImportSupportedChains,
22+
}
23+
24+
impl WalletImport {
25+
pub(crate) fn new(
26+
client: WalletsClient,
27+
initialization_response: WalletImportInitializationResponse,
28+
address: String,
29+
chain_type: WalletImportSupportedChains,
30+
) -> Self {
31+
Self {
32+
client,
33+
initialization_response,
34+
address,
35+
chain_type,
36+
}
37+
}
38+
39+
fn encrypt_private_key(
40+
&self,
41+
private_key_hex: &str,
42+
) -> Result<(String, String), Box<dyn std::error::Error>> {
43+
// Decode the public key from base64
44+
let public_key_bytes = base64::engine::general_purpose::STANDARD
45+
.decode(&self.initialization_response.encryption_public_key)?;
46+
47+
// Deserialize the public key using HPKE trait
48+
let public_key = <DhP256HkdfSha256 as hpke::Kem>::PublicKey::from_bytes(&public_key_bytes)
49+
.map_err(|e| format!("Failed to deserialize public key: {:?}", e))?;
50+
51+
// Convert hex private key to bytes (remove 0x prefix if present)
52+
let private_key_hex = private_key_hex
53+
.strip_prefix("0x")
54+
.unwrap_or(private_key_hex);
55+
let private_key_bytes = hex::decode(private_key_hex)?;
56+
57+
// Setup HPKE sender context
58+
let mut rng = rand::rng();
59+
let (encapsulated_key, mut encryption_context) =
60+
hpke::setup_sender::<ChaCha20Poly1305, HkdfSha256, DhP256HkdfSha256, _>(
61+
&OpModeS::Base,
62+
&public_key,
63+
&[],
64+
&mut rng,
65+
)
66+
.map_err(|e| format!("HPKE setup failed: {:?}", e))?;
67+
68+
// Encrypt the private key
69+
let ciphertext = encryption_context
70+
.seal(&private_key_bytes, &[])
71+
.map_err(|e| format!("HPKE encryption failed: {:?}", e))?;
72+
73+
// Encode results as base64
74+
let ciphertext_b64 = base64::engine::general_purpose::STANDARD.encode(&ciphertext);
75+
let encapsulated_key_b64 =
76+
base64::engine::general_purpose::STANDARD.encode(&encapsulated_key.to_bytes());
77+
78+
Ok((ciphertext_b64, encapsulated_key_b64))
79+
}
80+
81+
pub async fn submit(
82+
self,
83+
private_key_hex: &str,
84+
owner: Option<WalletImportSubmissionRequestOwner>,
85+
policy_ids: Vec<String>,
86+
additional_signers: Vec<
87+
crate::generated::types::WalletImportSubmissionRequestAdditionalSignersItem,
88+
>,
89+
) -> Result<ResponseValue<Wallet>, Error<()>> {
90+
// Encrypt the private key using HPKE
91+
let (ciphertext, encapsulated_key) = self
92+
.encrypt_private_key(private_key_hex)
93+
.map_err(|_| Error::InvalidRequest("Failed to encrypt private key".to_string()))?;
94+
95+
// Create the wallet submission input
96+
let wallet_input = PrivateKeySubmitInput {
97+
address: self.address,
98+
chain_type: self.chain_type,
99+
ciphertext,
100+
encapsulated_key,
101+
encryption_type: self.initialization_response.encryption_type,
102+
entropy_type: crate::generated::types::PrivateKeySubmitInputEntropyType::PrivateKey,
103+
};
104+
105+
// Create the submission request
106+
let submission_request = WalletImportSubmissionRequest {
107+
wallet: WalletImportSubmissionRequestWallet::PrivateKeySubmitInput(wallet_input),
108+
owner,
109+
owner_id: None,
110+
policy_ids,
111+
additional_signers,
112+
};
113+
114+
// Submit the import request
115+
self.client.submit_import(&submission_request).await
116+
}
117+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use base64::{Engine, engine::general_purpose::STANDARD};
88

99
pub mod client;
1010
pub mod ethereum;
11+
pub mod import;
1112
pub mod privy_hpke;
1213
pub mod solana;
1314

src/subclients.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@
44
//! as well as some manual overrides for things that need the authctx,
55
//! following the stainless spec.
66
7+
use p256::elliptic_curve::SecretKey;
8+
79
use crate::{
8-
AuthorizationContext,
10+
AuthorizationContext, PrivyHpke,
911
ethereum::EthereumService,
1012
generate_authorization_signatures,
11-
generated::types::{Policy, UpdatePolicyBody, UpdatePolicyPolicyId},
13+
generated::types::{
14+
HpkeEncryption, Policy, PrivateKeyInitInput, UpdatePolicyBody, UpdatePolicyPolicyId,
15+
Wallet, WalletExportRequestBody, WalletImportSubmissionRequestAdditionalSignersItem,
16+
WalletImportSubmissionRequestOwner, WalletImportSupportedChains,
17+
},
18+
import::WalletImport,
1219
solana::SolanaService,
1320
};
1421

@@ -237,20 +244,64 @@ impl WalletsClient {
237244
&'a self,
238245
wallet_id: &'a str,
239246
ctx: &'a AuthorizationContext,
240-
body: &'a crate::generated::types::WalletExportRequestBody,
241-
) -> Result<ResponseValue<crate::generated::types::WalletExportResponseBody>, Error<()>> {
247+
) -> Result<SecretKey<p256::NistP256>, Error<()>> {
248+
let privy_hpke = PrivyHpke::new();
249+
let body = WalletExportRequestBody {
250+
encryption_type: HpkeEncryption::Hpke,
251+
recipient_public_key: privy_hpke.public_key().unwrap(),
252+
};
253+
242254
let sig = generate_authorization_signatures(
243255
ctx,
244256
&self.app_id,
245257
crate::Method::POST,
246258
format!("{}/v1/wallets/{}/export", self.base_url, wallet_id),
247-
body,
259+
&body,
248260
None,
249261
)
250262
.await
251263
.unwrap();
252264

253-
self._export(wallet_id, Some(&sig), body).await
265+
let resp = self._export(wallet_id, Some(&sig), &body).await?;
266+
267+
Ok(privy_hpke
268+
.decrypt(&resp.encapsulated_key, &resp.ciphertext)
269+
.unwrap())
270+
}
271+
272+
pub async fn import(
273+
&self,
274+
address: String,
275+
private_key_hex: &str,
276+
chain_type: WalletImportSupportedChains,
277+
owner: Option<WalletImportSubmissionRequestOwner>,
278+
policy_ids: Vec<String>,
279+
additional_signers: Vec<WalletImportSubmissionRequestAdditionalSignersItem>,
280+
) -> Result<ResponseValue<Wallet>, progenitor_client::Error<()>> {
281+
let init_request =
282+
crate::generated::types::WalletImportInitializationRequest::PrivateKeyInitInput(
283+
PrivateKeyInitInput {
284+
address: address.clone(),
285+
chain_type: chain_type.clone(),
286+
encryption_type: HpkeEncryption::Hpke,
287+
entropy_type:
288+
crate::generated::types::PrivateKeyInitInputEntropyType::PrivateKey,
289+
},
290+
);
291+
292+
let response = self._init_import(&init_request).await?;
293+
let import = WalletImport::new(self.clone(), response.into_inner(), address, chain_type);
294+
295+
import
296+
.submit(private_key_hex, owner, policy_ids, additional_signers)
297+
.await
298+
}
299+
300+
pub(crate) async fn submit_import<'a>(
301+
&'a self,
302+
body: &'a types::WalletImportSubmissionRequest,
303+
) -> Result<ResponseValue<types::Wallet>, Error<()>> {
304+
self._submit_import(body).await
254305
}
255306

256307
pub fn ethereum(&self) -> EthereumService {

0 commit comments

Comments
 (0)