Implementing a Secret Vault in Rust
Hey there, fellow Rustacean!
Hey there, fellow Rustacean!
Today, we’re stepping into the realm of security. If you’ve ever caught yourself thinking about where to stash those digital secrets — API keys, credentials, you name it — we’re in the same boat. We’re all in for a treat because we’re about to scratch that itch by creating our own secret vault in Rust!
So, buckle up! We’re about to dive into some cryptography, play around with key derivations, and even wrestle with user input, all to keep our most prized digital possessions under lock and key.
The Building Blocks: Essential Crates
To make our vault implementation seamless and robust, we’ll be leveraging some of Rust’s top-notch crates:
aes-gcm
:
Dive into symmetric encryption withaes-gcm
. This crate rolls out the AES-GCM encryption algorithm, ensuring both the confidentiality and integrity of your treasured data. Part of the RustCrypto project, it's like the impenetrable wall for our digital fortress.rand
:
Ever needed a genuine element of surprise? Enterrand
, Rust's premier choice for random number generation. Whether it's cryptographically secure randomness for key creation or a simple dice roll,rand
is your go-to.serde
&serde_derive
:
Serialization is no hassle withserde
and its partnerserde_derive
. Convert complex Rust data types to various data formats effortlessly. It's like having a universal language translator for your data!bincode
:
Partnering withserde
,bincode
ensures efficient binary serialization. Think of it as the express lane for data interchange—compact, swift, and highly efficient, especially when you need data interactions at lightning speed.orion
:
The cornerstone of our vault's cryptography is theorion
crate. Offering an array of cryptographic tools, including the stalwart Argon2 key derivation function, it's the keymaster ensuring our vault's keys are resilient and secure.
Setting up a new Rust project
cargo new secret_vault
cd secret_vault
Adding Dependencies
Let’s add the following crates to our Cargo.toml file:
[dependencies]
aes-gcm = "0.10.3"
rand = "0.8"
serde = "1.0"
serde_derive = "1.0"
bincode = "1.3"
orion = "0.17.6"
rust-crypto = "0.2"
The Vault
module
The backbone of our vault system is the Vault
module:
extern crate crypto;
use crypto::pbkdf2::pbkdf2;
use crypto::sha2::Sha256;
use aes_gcm::KeyInit;
use aes_gcm::Aes256Gcm;
use aes_gcm::aead::{Aead};
use rand::Rng;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crypto::hmac::Hmac;
use serde_derive::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct SecretVault {
secrets: HashMap<String, Vec<u8>>,
}
impl SecretVault {
pub fn new() -> Self {
SecretVault {
secrets: HashMap::new(),
}
}
pub fn from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
let data = fs::read(path)?;
let vault: SecretVault = bincode::deserialize(&data)?;
Ok(vault)
}
pub fn save_to_file(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let data = bincode::serialize(&self)?;
fs::write(path, data)?;
Ok(())
}
pub fn derive_key(passphrase: &str) -> [u8; 32] {
let salt: [u8; 16] = rand::thread_rng().gen();
// Using pbkdf2 with Hmac and Sha256 to derive the key
let mut key = [0u8; 32];
let mut mac = Hmac::new(Sha256::new(), passphrase.as_bytes());
pbkdf2(&mut mac, &salt, 10000, &mut key); // 10000 iterations
key
}
// The following functions now take an encryption_key parameter:
pub fn add_secret(&mut self, encryption_key: &[u8; 32], key: String, secret: String) {
let cipher = Aes256Gcm::new_from_slice(encryption_key).unwrap();
let nonce: [u8; 12] = rand::thread_rng().gen();
let encrypted_secret = cipher.encrypt(&nonce.into(), secret.as_bytes()).unwrap();
let mut combined = nonce.to_vec();
combined.extend(encrypted_secret);
self.secrets.insert(key, combined);
}
pub fn get_secret(&self, encryption_key: &[u8; 32], key: &str) -> Option<String> {
if let Some(data) = self.secrets.get(key) {
let cipher = Aes256Gcm::new_from_slice(encryption_key).unwrap();
let (nonce, encrypted_secret) = data.split_at(12);
if let Ok(decrypted_secret) = cipher.decrypt(nonce.into(), encrypted_secret) {
return Some(String::from_utf8(decrypted_secret).unwrap());
}
}
None
}
pub fn remove_secret(&mut self, encryption_key: &[u8; 32], key: &str) -> Option<String> {
if let Some(data) = self.secrets.remove(key) {
let cipher = Aes256Gcm::new_from_slice(encryption_key).unwrap();
let (nonce, encrypted_secret) = data.split_at(12);
if let Ok(decrypted_secret) = cipher.decrypt(nonce.into(), encrypted_secret) {
return Some(String::from_utf8(decrypted_secret).unwrap());
}
}
None
}
}
Functions Breakdown
Let’s now understand each function in the code above.
This function initializes an empty vault:
pub fn new() -> Self {
SecretVault {
secrets: HashMap::new(),
}
}
The save_to_file function takes the current SecretVault
instance, serializes it using bincode
, and then writes that serialized data to the specified file. It's the way you persist the vault's state to disk for later retrieval:
pub fn save_to_file(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let data = bincode::serialize(&self)?;
fs::write(path, data)?;
Ok(())
}
The from_file function reads serialized data from the specified file path and deserializes it into a SecretVault
object using the bincode
library. It's your go-to method for loading the vault's state from disk:
pub fn from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
let data = fs::read(path)?;
let vault: SecretVault = bincode::deserialize(&data)?;
Ok(vault)
}
Next, derive_key employs PBKDF2 with HMAC-SHA256 to derive a 32-byte encryption key from the provided passphrase. The random salt ensures that the derived key is unique even for the same passphrase, bolstering security:
pub fn derive_key(passphrase: &str) -> [u8; 32] {
let salt: [u8; 16] = rand::thread_rng().gen();
// Using pbkdf2 with Hmac and Sha256 to derive the key
let mut key = [0u8; 32];
let mut mac = Hmac::new(Sha256::new(), passphrase.as_bytes());
pbkdf2(&mut mac, &salt, 10000, &mut key); // 10000 iterations
key
}
add_secret encrypts the input secret using AES-256-GCM with a randomly generated nonce. This nonce, combined with the encrypted secret, is then stored in the secrets
HashMap under the provided key. The AES encryption ensures the secret remains confidential:
pub fn add_secret(&mut self, encryption_key: &[u8; 32], key: String, secret: String) {
let cipher = Aes256Gcm::new_from_slice(encryption_key).unwrap();
let nonce: [u8; 12] = rand::thread_rng().gen();
let encrypted_secret = cipher.encrypt(&nonce.into(), secret.as_bytes()).unwrap();
let mut combined = nonce.to_vec();
combined.extend(encrypted_secret);
self.secrets.insert(key, combined);
}
The get_secret function attempts to fetch and decrypt a stored secret. Using the provided encryption key, it locates the secret by its key in the HashMap, splits the nonce from the encrypted data, and then decrypts it using AES-256-GCM:
pub fn get_secret(&self, encryption_key: &[u8; 32], key: &str) -> Option<String> {
if let Some(data) = self.secrets.get(key) {
let cipher = Aes256Gcm::new_from_slice(encryption_key).unwrap();
let (nonce, encrypted_secret) = data.split_at(12);
if let Ok(decrypted_secret) = cipher.decrypt(nonce.into(), encrypted_secret) {
return Some(String::from_utf8(decrypted_secret).unwrap());
}
}
None
}
remove_secret, similar to get_secret
, but with an added step: after decrypting, it removes the secret from the secrets
HashMap. It’s the method to use if you want to retrieve a secret one last time before discarding it:
pub fn remove_secret(&mut self, encryption_key: &[u8; 32], key: &str) -> Option<String> {
if let Some(data) = self.secrets.remove(key) {
let cipher = Aes256Gcm::new_from_slice(encryption_key).unwrap();
let (nonce, encrypted_secret) = data.split_at(12);
if let Ok(decrypted_secret) = cipher.decrypt(nonce.into(), encrypted_secret) {
return Some(String::from_utf8(decrypted_secret).unwrap());
}
}
None
}
Adding a command-line interface to interact with the vault
// src/main.rs
mod vault;
use vault::SecretVault;
use std::io::{self, Write};
use std::path::Path;
fn main() {
print!("Enter passphrase: ");
io::stdout().flush().unwrap();
let mut passphrase = String::new();
io::stdin().read_line(&mut passphrase).unwrap();
let encryption_key = SecretVault::derive_key(&passphrase.trim());
let mut secret_vault = SecretVault::new();
loop {
let mut input = String::new();
print!("vault> ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut input).unwrap();
let parts: Vec<&str> = input.trim().split_whitespace().collect();
match parts.as_slice() {
["add", key, secret] => {
secret_vault.add_secret(&encryption_key, key.to_string(), secret.to_string());
println!("Secret added!");
},
["get", key] => {
if let Some(secret) = secret_vault.get_secret(&encryption_key, key) {
println!("Secret: {}", secret);
} else {
println!("No secret found for key: {}", key);
}
},
["remove", key] => {
if secret_vault.remove_secret(&encryption_key, key).is_some() {
println!("Secret removed!");
} else {
println!("No secret found for key: {}", key);
}
},
["save", file_path] => {
if secret_vault.save_to_file(Path::new(file_path)).is_ok() {
println!("Vault saved to {}", file_path);
} else {
println!("Error saving vault to {}", file_path);
}
},
["load", file_path] => {
match SecretVault::from_file(Path::new(file_path)) {
Ok(vault) => {
secret_vault = vault;
println!("Vault loaded from {}", file_path);
},
Err(_) => println!("Error loading vault from {}", file_path),
}
},
["exit"] => {
break;
},
_ => {
println!("Invalid command!");
}
}
}
}
Commands:
- Add a Secret:
- Command:
add [key] [secret]
- Example:
add myAPIKey 1234567890
- This command encrypts and stores the provided secret under the specified key.
2. Retrieve a Secret:
- Command:
get [key]
- Example:
get myAPIKey
- This command retrieves and decrypts the secret associated with the provided key.
3. Remove a Secret:
- Command:
remove [key]
- Example:
remove myAPIKey
- This command deletes the secret associated with the specified key from the vault.
4. Save the Vault to a File:
- Command:
save [file_path]
- Example:
save /path/to/myvault.dat
- This command serializes and saves the current state of the vault to a file. The file doesn’t contain your encryption key.
5. Load the Vault from a File:
- Command:
load [file_path]
- Example:
load /path/to/myvault.dat
- This command loads a previously saved vault. You’ll need to provide the passphrase that was in use when the vault was saved to access its contents.
Wrapping Things Up
We’ve journeyed together through the creation and testing of SecretVault
, a simple tool designed to help secure your secrets in Rust. As with any piece of software, especially one dealing with encryption and security, there's always room to grow and refine.
This implementation serves as a foundational step, a basic blueprint for those delving into Rust and encryption. However, before deploying something similar in a real-world application, consider the following potential areas of improvement:
- Enhanced Key Management: The current approach relies on passphrase-derived keys. Integrating with hardware-based key storage or other advanced key management solutions could elevate the security significantly.
- Error Handling: While the code provides some basic error handling, a production-grade solution would need more comprehensive error management and logging.
- Additional Cryptographic Features: Features like key rotation, support for additional algorithms, or integration with trusted platform modules can be beneficial.
Always remember, building secure systems is an iterative process. This SecretVault
was designed primarily for learning purposes, offering a peek into the world of encryption in Rust. As you continue your journey, always be inquisitive, stay updated with the latest in cryptographic standards, and never stop refining and improving.
You can find the complete implementation over at my GitHub repository: https://github.com/luishsr/rust-secret-vault
Your feedback, suggestions, or contributions are always welcome.
Check out some interesting hands-on Rust articles!
🌟 Developing a Fully Functional API Gateway in Rust — Discover how to set up a robust and scalable gateway that stands as the frontline for your microservices.
🌟 Implementing a Network Traffic Analyzer — Ever wondered about the data packets zooming through your network? Unravel their mysteries with this deep dive into network analysis.
🌟 Building an Application Container in Rust — Join us in creating a lightweight, performant, and secure container from scratch! Docker’s got nothing on this. 😉
🌟 Crafting a Secure Server-to-Server Handshake with Rust & OpenSSL —
If you’ve been itching to give your servers a unique secret handshake that only they understand, you’ve come to the right place. Today, we’re venturing into the world of secure server-to-server handshakes, using the powerful combo of Rust and OpenSSL.
🌟 Building a Function-as-a-Service (FaaS) in Rust: If you’ve been exploring cloud computing, you’ve likely come across FaaS platforms like AWS Lambda or Google Cloud Functions. In this article, we’ll be creating our own simple FaaS platform using Rust.
🌟 Implementing a P2P Database in Rust: Today, we’re going to roll up our sleeves and get our hands dirty building a Peer-to-Peer (P2P) key-value database.
🌟 Implementing a VPN Server in Rust: Interested in understanding the inner workings of a VPN? Thinking about setting up your own VPN server? Today, we’re taking a straightforward look at how to set up a basic VPN server using Rust.
Happy coding, and keep those Rust gears turning! 🦀
Read more articles about Rust in my Rust Programming Library!
Visit my Blog for more articles, news, and software engineering stuff!
Follow me on Medium, LinkedIn, and Twitter.
Leave a comment, and drop me a message!
All the best,
Luis Soares
CTO | Tech Lead | Senior Software Engineer | Cloud Solutions Architect | Rust 🦀 | Golang | Java | ML AI & Statistics | Web3 & Blockchain