This repository has been archived by the owner on Feb 3, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9fc2261
commit 923c4ab
Showing
6 changed files
with
355 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,332 @@ | ||
use crate::{ | ||
auth::MutinyAuthClient, | ||
error::MutinyError, | ||
key::{create_root_child_key, ChildKey}, | ||
onchain::coin_type_from_network, | ||
}; | ||
use crate::{logging::MutinyLogger, storage::MutinyStorage}; | ||
use async_lock::RwLock; | ||
use bitcoin::{ | ||
bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}, | ||
secp256k1::Secp256k1, | ||
Network, | ||
}; | ||
use fedimint_client::derivable_secret::{ChildId, DerivableSecret}; | ||
use lightning::log_error; | ||
use lightning::util::logger::Logger; | ||
use reqwest::Method; | ||
use serde::{Deserialize, Serialize}; | ||
use std::{collections::HashMap, sync::Arc}; | ||
use tbs::{blind_message, BlindedMessage, BlindedSignature, BlindingKey}; | ||
use url::Url; | ||
|
||
const BLINDAUTH_CLIENT_NONCE: &[u8] = b"BlindAuth Client Salt"; | ||
|
||
/// The type of blinded message this is for | ||
const SERVICE_REGISTRATION_CHILD_ID: ChildId = ChildId(0); | ||
|
||
/// Child ID used to derive the spend key from a service plan's DerivableSecret | ||
const SPEND_KEY_CHILD_ID: ChildId = ChildId(0); | ||
|
||
/// Child ID used to derive the blinding key from a service plan's DerivableSecret | ||
const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1); | ||
|
||
#[derive(Debug, Serialize, Deserialize, Clone, Default)] | ||
pub struct TokenStorage { | ||
// (service_id, plan_id): number of times used | ||
pub map: HashMap<(u32, u32), u32>, | ||
pub tokens: Vec<BlindedToken>, | ||
pub version: u32, | ||
} | ||
|
||
impl TokenStorage { | ||
fn increment(&mut self, index: u32, spot: u32, token: BlindedToken) { | ||
let value = self.map.entry((index, spot)).or_insert(0); | ||
*value += 1; | ||
self.tokens.push(token); | ||
self.version += 1; | ||
} | ||
|
||
fn get_value(&self, index: u32, spot: u32) -> u32 { | ||
self.map.get(&(index, spot)).unwrap_or(&0).to_owned() | ||
} | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)] | ||
pub struct NotYetBlindedToken { | ||
pub counter: u32, | ||
pub service_id: u32, | ||
pub plan_id: u32, | ||
pub blinded_message: BlindedMessage, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)] | ||
pub struct BlindedToken { | ||
pub counter: u32, | ||
pub service_id: u32, | ||
pub plan_id: u32, | ||
pub blinded_message: BlindedMessage, | ||
pub blind_sig: BlindedSignature, | ||
pub spent: bool, | ||
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub struct CheckServiceTokenResponse { | ||
pub tokens: Vec<ServicePlans>, | ||
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub struct ServicePlans { | ||
pub service: Service, | ||
pub plan: Plan, | ||
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub struct Service { | ||
pub id: u32, | ||
pub name: String, | ||
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub struct Plan { | ||
pub id: u32, | ||
pub service_id: u32, | ||
pub name: String, | ||
pub allocation_count: u32, | ||
pub allocation_type: String, | ||
pub subscription_plan_reference: Option<i32>, | ||
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub struct RedeemServiceTokenRequest { | ||
pub service_id: u32, | ||
pub plan_id: u32, | ||
pub blinded_message: BlindedMessage, | ||
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub struct RedeemServiceTokenResponse { | ||
pub service_id: u32, | ||
pub plan_id: u32, | ||
pub blind_sig: BlindedSignature, | ||
} | ||
|
||
pub struct BlindAuthClient<S: MutinyStorage> { | ||
secret: DerivableSecret, | ||
auth_client: Arc<MutinyAuthClient>, | ||
base_url: String, | ||
storage: S, | ||
token_storage: Arc<RwLock<TokenStorage>>, | ||
pub logger: Arc<MutinyLogger>, | ||
} | ||
|
||
impl<S: MutinyStorage> BlindAuthClient<S> { | ||
pub fn new( | ||
xprivkey: ExtendedPrivKey, | ||
auth_client: Arc<MutinyAuthClient>, | ||
network: Network, | ||
base_url: String, | ||
storage: &S, | ||
logger: Arc<MutinyLogger>, | ||
) -> Result<Self, MutinyError> { | ||
let token_storage = storage.get_token_storage()?; | ||
let secret = create_blind_auth_secret(xprivkey, network)?; | ||
|
||
Ok(Self { | ||
secret, | ||
auth_client, | ||
base_url, | ||
storage: storage.clone(), | ||
token_storage: Arc::new(RwLock::new(token_storage)), | ||
logger, | ||
}) | ||
} | ||
|
||
pub async fn redeem_available_tokens(&self) -> Result<(), MutinyError> { | ||
// check to see what is available to the user | ||
let available_tokens = self.check_available_tokens().await?; | ||
|
||
// fetch available one by one | ||
for service in available_tokens.tokens { | ||
match self.redeem_token(service).await { | ||
Ok(_) => (), | ||
Err(e) => { | ||
log_error!(self.logger, "could not redeem token: {e}"); | ||
} | ||
}; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
async fn check_available_tokens(&self) -> Result<CheckServiceTokenResponse, MutinyError> { | ||
get_available_tokens(&self.auth_client, &self.base_url).await | ||
} | ||
|
||
async fn redeem_token(&self, service: ServicePlans) -> Result<BlindedToken, MutinyError> { | ||
let service_id = service.service.id; | ||
let plan_id = service.plan.id; | ||
let mut token_storage_guard = self.token_storage.write().await; | ||
let next_counter = token_storage_guard.get_value(service_id, plan_id) + 1; | ||
|
||
// create the deterministic info to derive the token from | ||
let token_to_blind = | ||
derive_blind_token(&self.secret, service_id, plan_id, next_counter).await?; | ||
let token_req = RedeemServiceTokenRequest { | ||
service_id, | ||
plan_id, | ||
blinded_message: token_to_blind.blinded_message, | ||
}; | ||
|
||
// request a blinded signature | ||
let token_resp = post_redeem_tokens(&self.auth_client, &self.base_url, token_req).await?; | ||
let blinded_token = BlindedToken { | ||
counter: token_to_blind.counter, | ||
service_id, | ||
plan_id, | ||
blinded_message: token_to_blind.blinded_message, | ||
blind_sig: token_resp.blind_sig, | ||
spent: false, | ||
}; | ||
|
||
// store the complete blinded token info | ||
token_storage_guard.increment(service_id, plan_id, blinded_token.clone()); | ||
|
||
// FIXME what if storage fails remotely? Revert somehow? | ||
// It will at least be there locally | ||
// Maybe have an "issued" tokens call so we can see if we're caught up with the server? | ||
self.storage | ||
.insert_token_storage(token_storage_guard.clone()) | ||
.await?; | ||
|
||
Ok(blinded_token) | ||
} | ||
|
||
pub async fn available_tokens(&self) -> Vec<BlindedToken> { | ||
self.token_storage | ||
.read() | ||
.await | ||
.tokens | ||
.clone() | ||
.into_iter() | ||
.filter(|t| !t.spent) | ||
.collect::<Vec<BlindedToken>>() | ||
} | ||
|
||
pub async fn used_token(&self, token: BlindedToken) -> Result<(), MutinyError> { | ||
// once a token has sufficiently been used, mark it as spent and save it back | ||
let mut token_storage_guard = self.token_storage.write().await; | ||
|
||
// find the token in the vector of tokens | ||
if let Some(index) = token_storage_guard.tokens.iter_mut().position(|t| { | ||
t.service_id == token.service_id | ||
&& t.plan_id == token.plan_id | ||
&& t.counter == token.counter | ||
}) { | ||
// mark the found token as spent | ||
token_storage_guard.tokens[index].spent = true; | ||
token_storage_guard.version += 1; | ||
|
||
// save the updated token storage back to the database or other persistent storage | ||
self.storage | ||
.insert_token_storage(token_storage_guard.clone()) | ||
.await?; | ||
} else { | ||
return Err(MutinyError::NotFound); | ||
} | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
async fn get_available_tokens( | ||
auth_client: &MutinyAuthClient, | ||
base_url: &str, | ||
) -> Result<CheckServiceTokenResponse, MutinyError> { | ||
let url = Url::parse(&format!("{}/v1/check-tokens", base_url)) | ||
.map_err(|_| MutinyError::ConnectionFailed)?; | ||
let res = auth_client | ||
.request(Method::POST, url, None) | ||
.await? | ||
.json::<CheckServiceTokenResponse>() | ||
.await | ||
.map_err(|_| MutinyError::ConnectionFailed)?; | ||
|
||
Ok(res) | ||
} | ||
|
||
async fn post_redeem_tokens( | ||
auth_client: &MutinyAuthClient, | ||
base_url: &str, | ||
req: RedeemServiceTokenRequest, | ||
) -> Result<RedeemServiceTokenResponse, MutinyError> { | ||
let url = Url::parse(&format!("{}/v1/redeem-tokens", base_url)) | ||
.map_err(|_| MutinyError::ConnectionFailed)?; | ||
let body = serde_json::to_value(req)?; | ||
let res = auth_client | ||
.request(Method::POST, url, Some(body)) | ||
.await? | ||
.json::<RedeemServiceTokenResponse>() | ||
.await | ||
.map_err(|_| MutinyError::ConnectionFailed)?; | ||
|
||
Ok(res) | ||
} | ||
|
||
async fn derive_blind_token( | ||
secret: &DerivableSecret, | ||
service_id: u32, | ||
plan_id: u32, | ||
counter: u32, | ||
) -> Result<NotYetBlindedToken, MutinyError> { | ||
let child_secret = secret | ||
.child_key(SERVICE_REGISTRATION_CHILD_ID) | ||
.child_key(ChildId(service_id.into())) | ||
.child_key(ChildId(plan_id.into())) | ||
.child_key(ChildId(counter.into())); | ||
|
||
let spend_key = child_secret | ||
.child_key(SPEND_KEY_CHILD_ID) | ||
.to_secp_key(fedimint_ln_common::bitcoin::secp256k1::SECP256K1); | ||
|
||
let nonce = fedimint_mint_client::Nonce(spend_key.public_key()); | ||
let blinding_key = BlindingKey( | ||
child_secret | ||
.child_key(BLINDING_KEY_CHILD_ID) | ||
.to_bls12_381_key(), | ||
); | ||
let blinded_message = blind_message(nonce.to_message(), blinding_key); | ||
|
||
let blinded_token = NotYetBlindedToken { | ||
counter, | ||
service_id, | ||
plan_id, | ||
blinded_message, | ||
}; | ||
|
||
Ok(blinded_token) | ||
} | ||
|
||
// Creates the root derivation secret for the blind auth client: | ||
// `m/2'/N'` where `N` is the network type. | ||
// | ||
// Each specific service+plan will have a derivation from there. | ||
fn create_blind_auth_secret( | ||
xprivkey: ExtendedPrivKey, | ||
network: Network, | ||
) -> Result<DerivableSecret, MutinyError> { | ||
let context = Secp256k1::new(); | ||
|
||
let shared_key = create_root_child_key(&context, xprivkey, ChildKey::BlindAuthChildKey)?; | ||
let xpriv = shared_key.derive_priv( | ||
&context, | ||
&DerivationPath::from(vec![ChildNumber::from_hardened_idx( | ||
coin_type_from_network(network), | ||
)?]), | ||
)?; | ||
|
||
Ok(DerivableSecret::new_root( | ||
&xpriv.private_key.secret_bytes(), | ||
BLINDAUTH_CLIENT_NONCE, | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
extern crate core; | ||
|
||
pub mod auth; | ||
mod blindauth; | ||
mod cashu; | ||
mod chain; | ||
pub mod encrypt; | ||
|
Oops, something went wrong.