Skip to content

Instantly share code, notes, and snippets.

@ronanyeah
Created December 13, 2023 23:03
Show Gist options
  • Save ronanyeah/e771ece48ee2ca10e8cd8991f8d70d51 to your computer and use it in GitHub Desktop.
Save ronanyeah/e771ece48ee2ca10e8cd8991f8d70d51 to your computer and use it in GitHub Desktop.
Switchboard Function Secrets
[package]
name = "secrets_compile"
version = "0.1.0"
edition = "2021"
[dependencies]
aes-gcm = "0.10.3"
base64 = "0.21.5"
rand = "0.8.5"
reqwest = "0.11.22"
rsa = "0.9.6"
serde = "1.0.193"
serde_json = "1.0.108"
switchboard-solana = "0.29.73"
[patch.crates-io]
# https://github.com/solana-labs/solana/issues/26688#issuecomment-1841629159
aes-gcm-siv = { git = "https://github.com/RustCrypto/AEADs", rev = "6105d7a5591aefa646a95d12b5e8d3f55a9214ef" }
curve25519-dalek = { git = "https://github.com/dalek-cryptography/curve25519-dalek", rev = "8274d5cbb6fc3f38cdc742b4798173895cd2a290" }
use base64::Engine;
use std::collections::HashMap;
use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit, Nonce};
use rand::rngs::OsRng;
use reqwest;
use rsa::pkcs8::EncodePublicKey;
use rsa::{RsaPrivateKey, RsaPublicKey};
use serde::Deserialize;
use serde_json::json;
use switchboard_solana::*;
/// Represents encrypted data containing a key, nonce, and data.
///
/// This structure holds information necessary for decrypting an AES-encrypted payload.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
struct EncryptedData {
/// A base64 encoded string containing the key used to decrypt the `data`.
///
/// This key is itself encrypted with the request's public key and can be decrypted using the
/// corresponding private key.
key: String,
/// An AES nonce needed to decrypt the `data`.
///
/// This value is used alongside the key to ensure secure decryption.
nonce: String,
/// The response payload that has been encrypted with AES.
///
/// This data can be of any type, but using a binary format is recommended for efficiency.
data: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
pub struct SwitchboardSecret {
pub secrets: HashMap<String, String>,
}
fn handle_reqwest_err(e: reqwest::Error) -> SbError {
let status = e.status().unwrap_or(reqwest::StatusCode::default());
println!(
"reqwest_error: code = {}, message = {}",
status,
status.canonical_reason().unwrap_or("Unknown")
);
SbError::CustomError {
message: format!(
"reqwest_error: code = {}, message = {}",
status,
status.canonical_reason().unwrap_or("Unknown")
),
source: std::sync::Arc::new(e),
}
}
impl SwitchboardSecret {
/// Fetch all of a user's secrets that have been whitelisted for the currently running mrEnclave
/// value.
pub async fn fetch(user_pubkey: &str) -> Result<Self, SbError> {
// Generate quote for secure request with user's public key
let mut os_rng = OsRng::default();
let priv_key = RsaPrivateKey::new(&mut os_rng, 2048).map_err(|_| SbError::KeyParseError)?;
let pub_key = RsaPublicKey::from(&priv_key)
.to_public_key_der()
.map_err(|_| SbError::KeyParseError)?;
// The quote is generated around the public encryption key so that the server can validate
// that the request has not been tampered with.
let secrets_quote =
Gramine::generate_quote(pub_key.as_ref()).map_err(|_| SbError::SgxError)?;
let enc_key = pub_key
.to_pem("PUBLIC KEY", rsa::pkcs1::LineEnding::default())
.map_err(|_| SbError::KeyParseError)?;
// Build and send request to fetch encrypted secrets
let payload = json!({
"user_pubkey": user_pubkey,
"ciphersuite": "ed25519",
"encryption_key": enc_key,
"quote": &secrets_quote,
});
let response = reqwest::Client::new()
.post("https://api.secrets.switchboard.xyz/")
.json(&payload)
.send()
.await
.map_err(handle_reqwest_err)?
.error_for_status()
.map_err(handle_reqwest_err)?;
let encrypted_data = response
.json::<EncryptedData>()
.await
.map_err(handle_reqwest_err)?;
// First we need to decode and decrypt the encryption key.
let key = match base64::engine::general_purpose::STANDARD.decode(encrypted_data.key) {
Ok(value) => value,
Err(err) => {
let error_msg = format!("Base64DecodeError: {:#?}", err);
println!("{}", error_msg);
return Err(SbError::CustomMessage(error_msg));
}
};
let key = match priv_key.decrypt(rsa::pkcs1v15::Pkcs1v15Encrypt, &key) {
Ok(value) => Key::<Aes256Gcm>::clone_from_slice(&value),
Err(err) => {
let error_msg = format!("DecryptKeyError: {:#?}", err);
println!("{}", error_msg);
return Err(SbError::CustomMessage(error_msg));
}
};
// Second we need to decode the nonce value from the encrypted data.
let nonce = match base64::engine::general_purpose::STANDARD.decode(encrypted_data.nonce) {
Ok(value) => Nonce::clone_from_slice(&value),
Err(err) => {
let error_msg = format!("Base64DecodeError: {:#?}", err);
println!("{}", error_msg);
return Err(SbError::CustomMessage(error_msg));
}
};
// Lastly, we can use our decrypted key and nonce values to decode and decrypt the payload.
let data = match base64::engine::general_purpose::STANDARD.decode(encrypted_data.data) {
Ok(value) => value,
Err(err) => {
let error_msg = format!("Base64DecodeError: {:#?}", err);
println!("{}", error_msg);
return Err(SbError::CustomMessage(error_msg));
}
};
let data = match Aes256Gcm::new(&key).decrypt(&nonce, data.as_ref()) {
Ok(value) => value,
Err(err) => {
let error_msg = format!("Aes256GcmError: {:#?}", err);
println!("{}", error_msg);
return Err(SbError::CustomMessage(error_msg));
}
};
// The data can be parsed into a hashmap and returned.
let secrets: HashMap<String, String> = serde_json::from_slice(&data)?;
Ok(Self { secrets })
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment