Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimal secrets passing for nostr keys #372

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
190 changes: 47 additions & 143 deletions src/bin/bitmaskd.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mod wallet;

pub use crate::bitcoin::{
assets::dust_tx,
keys::{new_mnemonic, save_mnemonic, BitcoinKeysError},
keys::{new_mnemonic, save_mnemonic, BitcoinKeysError, NOSTR_SK},
payment::{create_payjoin, create_transaction, BitcoinPaymentError},
psbt::{sign_psbt, sign_psbt_with_multiple_wallets, BitcoinPsbtError},
wallet::{
Expand Down
58 changes: 39 additions & 19 deletions src/bitcoin/keys.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::str::FromStr;
use std::{pin::Pin, str::FromStr};

use bdk::{
bitcoin::{
Expand All @@ -13,8 +13,9 @@ use bitcoin::KeyPair;
use bitcoin_hashes::{sha256, Hash};
use miniscript_crate::DescriptorPublicKey;
use nostr_sdk::prelude::{FromSkStr, ToBech32};
use once_cell::sync::OnceCell;
use thiserror::Error;
use zeroize::Zeroize;
use zeroize::{Zeroize, Zeroizing};

use crate::{
constants::{BTC_PATH, NETWORK},
Expand Down Expand Up @@ -79,49 +80,66 @@ fn get_descriptor(
}
}

fn xprv_desc(xprv: &ExtendedPrivKey, path: &str, change: u32) -> Result<String, BitcoinKeysError> {
fn xprv_desc(
xprv: &ExtendedPrivKey,
path: &str,
change: u32,
) -> Result<SecretString, BitcoinKeysError> {
let xprv = get_descriptor(xprv, path, change)?;

Ok(format!("tr({xprv})"))
Ok(SecretString(format!("tr({xprv})")))
}

fn xpub_desc(xprv: &ExtendedPrivKey, path: &str, change: u32) -> Result<String, BitcoinKeysError> {
fn xpub_desc(
xprv: &ExtendedPrivKey,
path: &str,
change: u32,
) -> Result<SecretString, BitcoinKeysError> {
let secp = Secp256k1::new();
let xprv = get_descriptor(xprv, path, change)?;
let xpub = xprv.to_public(&secp)?;

Ok(format!("tr({xpub})"))
Ok(SecretString(format!("tr({xpub})")))
}

fn watcher_xpub(
xprv: &ExtendedPrivKey,
path: &str,
change: u32,
) -> Result<String, BitcoinKeysError> {
) -> Result<SecretString, BitcoinKeysError> {
let secp = Secp256k1::new();
let xprv = get_descriptor(xprv, path, change)?;
let xpub = xprv.to_public(&secp)?;

if let DescriptorPublicKey::XPub(desc) = xpub {
Ok(desc.xkey.to_string())
Ok(SecretString(desc.xkey.to_string()))
} else {
Err(BitcoinKeysError::UnexpectedWatcherXpubDescriptor)
}
}

pub static NOSTR_SK: OnceCell<Pin<Zeroizing<[u8; 32]>>> = OnceCell::new();

// For NIP-06 Nostr signing and Carbonado encryption key derivation
fn nostr_keypair(xprv: &ExtendedPrivKey) -> Result<(String, String), BitcoinKeysError> {
fn nostr_keypair(xprv: &ExtendedPrivKey) -> Result<(SecretString, SecretString), BitcoinKeysError> {
pub const NOSTR_PATH: &str = "m/44'/1237'/0'/0/0";
let deriv_descriptor = DerivationPath::from_str(NOSTR_PATH)?;
let secp = Secp256k1::new();
let nostr_sk = xprv.derive_priv(&secp, &deriv_descriptor)?;
let keypair =
KeyPair::from_seckey_slice(&secp, nostr_sk.private_key.secret_bytes().as_slice())?;

Ok((
hex::encode(nostr_sk.private_key.secret_bytes()),
hex::encode(keypair.x_only_public_key().0.serialize()),
))
let mut sk = nostr_sk.private_key.secret_bytes();
let _ = NOSTR_SK.set(Pin::new(Zeroizing::new(sk)));

let sk_str = SecretString(hex::encode(sk));
sk.zeroize();

let mut pk = keypair.x_only_public_key().0.serialize();
let pk_str = SecretString(hex::encode(pk));
pk.zeroize();

Ok((sk_str, pk_str))
}

pub async fn new_mnemonic(
Expand Down Expand Up @@ -152,11 +170,13 @@ pub async fn get_mnemonic(

let network = NETWORK.read().await;
let xprv = ExtendedPrivKey::new_master(*network, &seed)?;
let xprvkh = sha256::Hash::hash(&xprv.to_priv().to_bytes()).to_string();
let mut xprvkh_bytes = xprv.to_priv().to_bytes();
let xprvkh = SecretString(sha256::Hash::hash(&xprvkh_bytes).to_string());
xprvkh_bytes.zeroize();

let secp = Secp256k1::new();
let xpub = ExtendedPubKey::from_priv(&secp, &xprv);
let xpubkh = xpub.to_pub().pubkey_hash().to_string();
let xpubkh = SecretString(xpub.to_pub().pubkey_hash().to_string());

let btc_path = BTC_PATH.read().await;

Expand All @@ -172,9 +192,9 @@ pub async fn get_mnemonic(
let watcher_xpub = watcher_xpub(&xprv, &btc_path, 0)?;

let (nostr_prv, nostr_pub) = nostr_keypair(&xprv)?;
let nostr_keys = nostr_sdk::Keys::from_sk_str(&nostr_prv)?;
let nostr_nsec = nostr_keys.secret_key()?.to_bech32()?;
let nostr_npub = nostr_keys.public_key().to_bech32()?;
let nostr_keys = nostr_sdk::Keys::from_sk_str(&nostr_prv.0)?;
let nostr_nsec = SecretString(nostr_keys.secret_key()?.to_bech32()?);
let nostr_npub = SecretString(nostr_keys.public_key().to_bech32()?);

let private = PrivateWalletData {
xprvkh,
Expand All @@ -187,7 +207,7 @@ pub async fn get_mnemonic(
};

let public = PublicWalletData {
xpub: xpub.to_string(),
xpub: SecretString(xpub.to_string()),
xpubkh,
watcher_xpub,
btc_descriptor_xpub,
Expand Down
14 changes: 7 additions & 7 deletions src/bitcoin/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ use crate::{
pub enum BitcoinPaymentError {
/// Payjoin error response
#[error("Error performing payjoin: {0}")]
PayjoinError(String),
Payjoin(String),
/// BitMask Core Bitcoin Psbt error
#[error(transparent)]
BitcoinPsbtError(#[from] BitcoinPsbtError),
BitcoinPsbt(#[from] BitcoinPsbtError),
/// BDK error
#[error(transparent)]
BdkError(#[from] bdk::Error),
Bdk(#[from] bdk::Error),
/// Payjoin Request error
#[error(transparent)]
PayjoinGetRequestError(#[from] payjoin::send::CreateRequestError),
PayjoinGetRequest(#[from] payjoin::send::CreateRequestError),
/// Payjoin Send error
#[error(transparent)]
PayjoinSendError(#[from] payjoin::send::ValidationError),
PayjoinSend(#[from] payjoin::send::ValidationError),
/// Reqwest error
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),
Reqwest(#[from] reqwest::Error),
}

pub async fn create_transaction(
Expand Down Expand Up @@ -135,7 +135,7 @@ pub async fn create_payjoin(
info!(format!("Response: {res}"));

if res.contains("errorCode") {
return Err(BitcoinPaymentError::PayjoinError(format!("{res:?}")));
return Err(BitcoinPaymentError::Payjoin(format!("{res:?}")));
}

let payjoin_psbt = ctx.process_response(&mut res.as_bytes())?;
Expand Down
69 changes: 43 additions & 26 deletions src/carbonado.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub use server::{handle_file, retrieve, retrieve_metadata, store};

#[cfg(not(target_arch = "wasm32"))]
mod server {
use crate::bitcoin::NOSTR_SK;

use super::*;

use std::{
Expand All @@ -20,35 +22,40 @@ mod server {
use tokio::fs;

pub async fn store(
sk: &str,
name: &str,
input: &[u8],
_force: bool,
metadata: Option<Vec<u8>>,
) -> Result<(), CarbonadoError> {
let level = 15;
let sk = hex::decode(sk)?;
let secret_key = SecretKey::from_slice(&sk)?;

let sk = NOSTR_SK
.get()
.ok_or(CarbonadoError::NoSecretKey)?
.as_slice();
let secret_key = SecretKey::from_slice(sk)?;
let public_key = PublicKey::from_secret_key_global(&secret_key);
let pk = public_key.serialize();
let pk_hex = hex::encode(pk);

let meta: Option<[u8; 8]> = metadata.map(|m| m.try_into().expect("invalid metadata size"));
let (body, _encode_info) = carbonado::file::encode(&sk, Some(&pk), input, level, meta)?;
let (body, _encode_info) = carbonado::file::encode(sk, Some(&pk), input, level, meta)?;
let filepath = handle_file(&pk_hex, name, body.len()).await?;
fs::write(filepath, body).await?;
Ok(())
}

pub async fn retrieve(
sk: &str,
name: &str,
alt_names: Vec<&String>,
) -> Result<(Vec<u8>, Option<Vec<u8>>), CarbonadoError> {
use crate::rgb::constants::RGB_STRICT_TYPE_VERSION;

let sk = hex::decode(sk)?;
let secret_key = SecretKey::from_slice(&sk)?;
let sk = NOSTR_SK
.get()
.ok_or(CarbonadoError::NoSecretKey)?
.as_slice();
let secret_key = SecretKey::from_slice(sk)?;
let public_key = PublicKey::from_secret_key_global(&secret_key);
let pk = public_key.to_hex();

Expand All @@ -61,7 +68,7 @@ mod server {

let filepath = handle_file(&pk, &final_name, 0).await?;
if let Ok(bytes) = fs::read(filepath).await {
let (header, decoded) = carbonado::file::decode(&sk, &bytes)?;
let (header, decoded) = carbonado::file::decode(sk, &bytes)?;
return Ok((decoded, header.metadata.map(|m| m.to_vec())));
}

Expand All @@ -70,7 +77,7 @@ mod server {
for alt_name in alt_names {
let filepath = handle_file(&pk, &alt_name, 0).await?;
if let Ok(bytes) = fs::read(filepath).await {
let (header, decoded) = carbonado::file::decode(&sk, &bytes)?;
let (header, decoded) = carbonado::file::decode(sk, &bytes)?;
if let Some(metadata) = header.metadata {
if metadata == RGB_STRICT_TYPE_VERSION {
return Ok((decoded, header.metadata.map(|m| m.to_vec())));
Expand Down Expand Up @@ -119,9 +126,12 @@ mod server {
Ok(filepath)
}

pub async fn retrieve_metadata(sk: &str, name: &str) -> Result<FileMetadata, CarbonadoError> {
let sk = hex::decode(sk)?;
let secret_key = SecretKey::from_slice(&sk)?;
pub async fn retrieve_metadata(name: &str) -> Result<FileMetadata, CarbonadoError> {
let sk = NOSTR_SK
.get()
.ok_or(CarbonadoError::NoSecretKey)?
.as_slice();
let secret_key = SecretKey::from_slice(sk)?;
let public_key = PublicKey::from_secret_key_global(&secret_key);
let pk = public_key.to_hex();

Expand All @@ -136,7 +146,7 @@ mod server {
let filepath = handle_file(&pk, &final_name, 0).await?;
let bytes = fs::read(filepath).await?;

let (header, _) = carbonado::file::decode(&sk, &bytes)?;
let (header, _) = carbonado::file::decode(sk, &bytes)?;

let result = FileMetadata {
filename: header.file_name(),
Expand All @@ -162,7 +172,7 @@ mod client {
use gloo_net::http::Request;
use gloo_utils::errors::JsError;

use crate::constants::CARBONADO_ENDPOINT;
use crate::{bitcoin::NOSTR_SK, constants::CARBONADO_ENDPOINT};

fn js_to_error(js_value: JsValue) -> CarbonadoError {
CarbonadoError::JsError(js_to_js_error(js_value))
Expand All @@ -181,21 +191,23 @@ mod client {
}

pub async fn store(
sk: &str,
name: &str,
input: &[u8],
force: bool,
metadata: Option<Vec<u8>>,
) -> Result<(), CarbonadoError> {
let level = 15;
let sk = hex::decode(sk)?;
let secret_key = SecretKey::from_slice(&sk)?;
let sk = NOSTR_SK
.get()
.ok_or(CarbonadoError::NoSecretKey)?
.as_slice();
let secret_key = SecretKey::from_slice(sk)?;
let public_key = PublicKey::from_secret_key_global(&secret_key);
let pk = public_key.serialize();
let pk_hex = hex::encode(pk);

let meta: Option<[u8; 8]> = metadata.map(|m| m.try_into().expect("invalid metadata size"));
let (body, _encode_info) = carbonado::file::encode(&sk, Some(&pk), input, level, meta)?;
let (body, _encode_info) = carbonado::file::encode(sk, Some(&pk), input, level, meta)?;
let body = Arc::new(body);
let network = NETWORK.read().await.to_string();

Expand Down Expand Up @@ -228,9 +240,12 @@ mod client {
}
}

pub async fn retrieve_metadata(sk: &str, name: &str) -> Result<FileMetadata, CarbonadoError> {
let sk = hex::decode(sk)?;
let secret_key = SecretKey::from_slice(&sk)?;
pub async fn retrieve_metadata(name: &str) -> Result<FileMetadata, CarbonadoError> {
let sk = NOSTR_SK
.get()
.ok_or(CarbonadoError::NoSecretKey)?
.as_slice();
let secret_key = SecretKey::from_slice(sk)?;
let public_key = PublicKey::from_secret_key_global(&secret_key);
let pk = public_key.to_hex();

Expand All @@ -256,14 +271,16 @@ mod client {
}

pub async fn retrieve(
sk: &str,
name: &str,
alt_names: Vec<&String>,
) -> Result<(Vec<u8>, Option<Vec<u8>>), CarbonadoError> {
use carbonado::file::Header;

let sk = hex::decode(sk)?;
let secret_key = SecretKey::from_slice(&sk)?;
let sk = NOSTR_SK
.get()
.ok_or(CarbonadoError::NoSecretKey)?
.as_slice();
let secret_key = SecretKey::from_slice(sk)?;
let public_key = PublicKey::from_secret_key_global(&secret_key);
let pk = public_key.to_hex();

Expand All @@ -286,7 +303,7 @@ mod client {
let encoded = array.to_vec();

if encoded.len() > Header::len() {
let (header, decoded) = carbonado::file::decode(&sk, &encoded)?;
let (header, decoded) = carbonado::file::decode(sk, &encoded)?;
return Ok((decoded, header.metadata.map(|m| m.to_vec())));
}

Expand All @@ -308,7 +325,7 @@ mod client {
let encoded = array.to_vec();

if encoded.len() > Header::len() {
let (header, decoded) = carbonado::file::decode(&sk, &encoded)?;
let (header, decoded) = carbonado::file::decode(sk, &encoded)?;
return Ok((decoded, header.metadata.map(|m| m.to_vec())));
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/carbonado/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ pub enum CarbonadoError {
AllEndpointsFailed,
/// Debug: {0}
Debug(String),
/// No secret key available in memory
NoSecretKey,
}
12 changes: 11 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
#[macro_use]
extern crate amplify;

pub mod bitcoin;
mod bitcoin;

// Explicit exports to keep secrets private
pub use crate::bitcoin::{
create_payjoin, create_transaction, decrypt_wallet, drain_wallet, dust_tx, encrypt_wallet,
fund_vault, get_assets_vault, get_blockchain, get_new_address, get_wallet, get_wallet_data,
hash_password, new_mnemonic, new_wallet, save_mnemonic, send_sats, sign_psbt, sign_psbt_file,
sign_psbt_with_multiple_wallets, sync_wallet, sync_wallets, upgrade_wallet,
versioned_descriptor, BitcoinError,
};

pub mod carbonado;
pub mod constants;
pub mod error;
Expand Down
Loading