feat(deps): Change base64 to base64ct crate (#295)

This commit is contained in:
Prabhpreet Dua
2024-05-06 21:14:10 +05:30
committed by GitHub
parent 761d5730af
commit 4bb3153761
13 changed files with 546 additions and 91 deletions

13
Cargo.lock generated
View File

@@ -166,10 +166,10 @@ dependencies = [
]
[[package]]
name = "base64"
version = "0.21.7"
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bindgen"
@@ -1639,11 +1639,13 @@ dependencies = [
"allocator-api2",
"allocator-api2-tests",
"anyhow",
"base64ct",
"log",
"memsec",
"rand",
"rosenpass-to",
"rosenpass-util",
"tempfile",
"zeroize",
]
@@ -1659,9 +1661,10 @@ name = "rosenpass-util"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"base64ct",
"static_assertions",
"typenum",
"zeroize",
]
[[package]]
@@ -1669,7 +1672,7 @@ name = "rp"
version = "0.2.1"
dependencies = [
"anyhow",
"base64",
"base64ct",
"ctrlc-async",
"futures",
"futures-util",

View File

@@ -32,12 +32,8 @@ rosenpass-ciphers = { path = "ciphers" }
rosenpass-to = { path = "to" }
rosenpass-secret-memory = { path = "secret-memory" }
rosenpass-oqs = { path = "oqs" }
criterion = "0.4.0"
test_bin = "0.4.0"
libfuzzer-sys = "0.4"
stacker = "0.1.15"
doc-comment = "0.3.3"
base64 = "0.21.7"
base64ct = {version = "1.6.0", default-features=false}
zeroize = "1.7.0"
memoffset = "0.9.1"
thiserror = "1.0.59"
@@ -46,7 +42,6 @@ env_logger = "0.10.2"
toml = "0.7.8"
static_assertions = "1.1.0"
allocator-api2 = "0.2.14"
allocator-api2-tests = "0.2.15"
memsec = "0.6.3"
rand = "0.8.5"
typenum = "1.17.0"
@@ -61,5 +56,13 @@ blake2 = "0.10.6"
chacha20poly1305 = { version = "0.10.1", default-features = false, features = [ "std", "heapless" ] }
zerocopy = { version = "0.7.32", features = ["derive"] }
home = "0.5.9"
serial_test = "3.1.1"
derive_builder = "0.20.0"
#Dev dependencies
serial_test = "3.1.1"
tempfile="3"
stacker = "0.1.15"
libfuzzer-sys = "0.4"
test_bin = "0.4.0"
criterion = "0.4.0"
allocator-api2-tests = "0.2.15"

View File

@@ -5,10 +5,9 @@ use derive_builder::Builder;
use log::{debug, error, info, warn};
use mio::Interest;
use mio::Token;
use rosenpass_util::file::{fopen_w, Visibility};
use rosenpass_util::file::{StoreValueB64, StoreValueB64Writer};
use std::cell::Cell;
use std::io::Write;
use std::io::ErrorKind;
use std::net::Ipv4Addr;
@@ -31,7 +30,10 @@ use crate::{
protocol::{CryptoServer, MsgBuf, PeerPtr, SPk, SSk, SymKey, Timing},
};
use rosenpass_util::attempt;
use rosenpass_util::b64::{b64_writer, fmt_b64};
use rosenpass_util::b64::B64Display;
const MAX_B64_KEY_SIZE: usize = 32 * 5 / 3;
const MAX_B64_PEER_ID_SIZE: usize = 32 * 5 / 3;
const IPV4_ANY_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
const IPV6_ANY_ADDR: Ipv6Addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
@@ -746,7 +748,7 @@ impl AppServer {
KeyOutputReason::Exchanged => "Exchanged key with peer",
KeyOutputReason::Stale => "Erasing outdated key from peer",
};
info!("{} {}", msg, fmt_b64(&*peerid));
info!("{} {}", msg, peerid.fmt_b64::<MAX_B64_PEER_ID_SIZE>());
}
if let Some(of) = ap.outfile.as_ref() {
@@ -757,7 +759,7 @@ impl AppServer {
// data will linger in the linux page cache anyways with the current
// implementation, going to great length to erase the secret here is
// not worth it right now.
b64_writer(fopen_w(of, Visibility::Secret)?).write_all(key.secret())?;
key.store_b64::<MAX_B64_KEY_SIZE, _>(of)?;
let why = match why {
KeyOutputReason::Exchanged => "exchanged",
KeyOutputReason::Stale => "stale",
@@ -767,7 +769,7 @@ impl AppServer {
// it is meant to allow external detection of a successful key-exchange
println!(
"output-key peer {} key-file {of:?} {why}",
fmt_b64(&*peerid)
peerid.fmt_b64::<MAX_B64_PEER_ID_SIZE>()
);
}
@@ -792,7 +794,10 @@ impl AppServer {
}
}
};
b64_writer(child.stdin.take().unwrap()).write_all(key.secret())?;
if let Err(e) = key.store_b64_writer::<MAX_B64_KEY_SIZE, _>(child.stdin.take().unwrap())
{
error!("could not write psk to wg: {:?}", e);
}
thread::spawn(move || {
let status = child.wait();

View File

@@ -300,6 +300,7 @@ impl CliCommand {
config: config::Rosenpass,
test_helpers: Option<AppServerTest>,
) -> anyhow::Result<()> {
const MAX_PSK_SIZE: usize = 1000;
// load own keys
let sk = SSk::load(&config.secret_key)?;
let pk = SPk::load(&config.public_key)?;
@@ -316,7 +317,10 @@ impl CliCommand {
for cfg_peer in config.peers {
srv.add_peer(
// psk, pk, outfile, outwg, tx_addr
cfg_peer.pre_shared_key.map(SymKey::load_b64).transpose()?,
cfg_peer
.pre_shared_key
.map(SymKey::load_b64::<MAX_PSK_SIZE, _>)
.transpose()?,
SPk::load(&cfg_peer.public_key)?,
cfg_peer.key_out,
cfg_peer.wg.map(|cfg| app_server::WireguardOut {

View File

@@ -11,7 +11,7 @@ repository = "https://github.com/rosenpass/rosenpass"
[dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }
base64ct = { workspace = true }
x25519-dalek = { version = "2", features = ["static_secrets"] }
zeroize = { workspace = true }
@@ -34,5 +34,5 @@ netlink-packet-generic = "0.3"
netlink-packet-wireguard = "0.2"
[dev-dependencies]
tempfile = "3"
stacker = "0.1.15"
tempfile = {workspace = true}
stacker = {workspace = true}

View File

@@ -2,6 +2,7 @@ use std::{net::SocketAddr, path::PathBuf};
use anyhow::Result;
use crate::key::WG_B64_LEN;
#[derive(Default)]
pub struct ExchangePeer {
pub public_keys_dir: PathBuf,
@@ -160,7 +161,7 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> {
let wgsk_path = options.private_keys_dir.join("wgsk");
let wgsk = Secret::<WG_KEY_LEN>::load_b64(wgsk_path)?;
let wgsk = Secret::<WG_KEY_LEN>::load_b64::<WG_B64_LEN, _>(wgsk_path)?;
let mut attr: Vec<WgDeviceAttrs> = Vec::with_capacity(2);
attr.push(WgDeviceAttrs::PrivateKey(*wgsk.secret()));
@@ -221,7 +222,7 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> {
srv.add_peer(
if psk.exists() {
Some(SymKey::load_b64(psk))
Some(SymKey::load_b64::<WG_B64_LEN, _>(psk))
} else {
None
}

View File

@@ -1,19 +1,19 @@
use std::{
fs::{self, DirBuilder, OpenOptions},
io::Write,
os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt},
fs::{self, DirBuilder},
os::unix::fs::{DirBuilderExt, PermissionsExt},
path::Path,
};
use anyhow::{anyhow, Result};
use base64::Engine;
use rosenpass_util::file::LoadValueB64;
use rosenpass_util::file::{LoadValueB64, StoreValueB64};
use zeroize::Zeroize;
use rosenpass::protocol::{SPk, SSk};
use rosenpass_cipher_traits::Kem;
use rosenpass_ciphers::kem::StaticKem;
use rosenpass_secret_memory::{file::StoreSecret as _, Secret};
use rosenpass_secret_memory::{file::StoreSecret as _, Public, Secret};
pub const WG_B64_LEN: usize = 32 * 5 / 3;
#[cfg(not(target_family = "unix"))]
pub fn genkey(_: &Path) -> Result<()> {
@@ -45,18 +45,7 @@ pub fn genkey(private_keys_dir: &Path) -> Result<()> {
if !wgsk_path.exists() {
let wgsk: Secret<32> = Secret::random();
let mut wgsk_file = OpenOptions::new()
.write(true)
.create(true)
.mode(0o600)
.open(wgsk_path)?;
wgsk_file.write_all(
base64::engine::general_purpose::STANDARD
.encode(wgsk.secret())
.as_bytes(),
)?;
wgsk.store_b64::<WG_B64_LEN, _>(wgsk_path)?;
} else {
eprintln!(
"WireGuard secret key already exists at {:#?}: not regenerating",
@@ -92,18 +81,15 @@ pub fn pubkey(private_keys_dir: &Path, public_keys_dir: &Path) -> Result<()> {
let private_pqpk = private_keys_dir.join("pqpk");
let public_pqpk = public_keys_dir.join("pqpk");
let wgsk = Secret::load_b64(private_wgsk)?;
let mut wgpk: x25519_dalek::PublicKey = {
let wgsk = Secret::load_b64::<WG_B64_LEN, _>(private_wgsk)?;
let mut wgpk: Public<32> = {
let mut secret = x25519_dalek::StaticSecret::from(*wgsk.secret());
let public = x25519_dalek::PublicKey::from(&secret);
secret.zeroize();
public
Public::from_slice(public.as_bytes())
};
fs::write(
public_wgpk,
base64::engine::general_purpose::STANDARD.encode(wgpk.as_bytes()),
)?;
wgpk.store_b64::<WG_B64_LEN, _>(public_wgpk)?;
wgpk.zeroize();
fs::copy(private_pqpk, public_pqpk)?;
@@ -115,12 +101,13 @@ pub fn pubkey(private_keys_dir: &Path, public_keys_dir: &Path) -> Result<()> {
mod tests {
use std::fs;
use base64::Engine;
use rosenpass::protocol::{SPk, SSk};
use rosenpass_secret_memory::Secret;
use rosenpass_util::file::LoadValue;
use rosenpass_util::file::LoadValueB64;
use tempfile::tempdir;
use crate::key::{genkey, pubkey};
use crate::key::{genkey, pubkey, WG_B64_LEN};
#[test]
fn it_works() {
@@ -136,9 +123,9 @@ mod tests {
assert!(private_keys_dir.path().is_dir());
assert!(SPk::load(private_keys_dir.path().join("pqpk")).is_ok());
assert!(SSk::load(private_keys_dir.path().join("pqsk")).is_ok());
assert!(base64::engine::general_purpose::STANDARD
.decode(&fs::read_to_string(private_keys_dir.path().join("wgsk")).unwrap())
.is_ok());
assert!(
Secret::<32>::load_b64::<WG_B64_LEN, _>(private_keys_dir.path().join("wgsk")).is_ok()
);
let public_keys_dir = tempdir().unwrap();
fs::remove_dir(public_keys_dir.path()).unwrap();
@@ -151,9 +138,9 @@ mod tests {
assert!(public_keys_dir.path().exists());
assert!(public_keys_dir.path().is_dir());
assert!(SPk::load(public_keys_dir.path().join("pqpk")).is_ok());
assert!(base64::engine::general_purpose::STANDARD
.decode(&fs::read_to_string(public_keys_dir.path().join("wgpk")).unwrap())
.is_ok());
assert!(
Secret::<32>::load_b64::<WG_B64_LEN, _>(public_keys_dir.path().join("wgpk")).is_ok()
);
let pk_1 = fs::read(private_keys_dir.path().join("pqpk")).unwrap();
let pk_2 = fs::read(public_keys_dir.path().join("pqpk")).unwrap();

View File

@@ -21,3 +21,5 @@ log = { workspace = true }
[dev-dependencies]
allocator-api2-tests = { workspace = true }
tempfile = {workspace = true}
base64ct = {workspace = true}

View File

@@ -1,10 +1,16 @@
use crate::debug::debug_crypto_array;
use anyhow::Context;
use rand::{Fill as Randomize, Rng};
use rosenpass_to::{ops::copy_slice, To};
use rosenpass_util::file::{fopen_r, LoadValue, ReadExactToEnd, StoreValue};
use rosenpass_util::b64::{b64_decode, b64_encode};
use rosenpass_util::file::{
fopen_r, fopen_w, LoadValue, LoadValueB64, ReadExactToEnd, ReadSliceToEnd, StoreValue,
StoreValueB64, StoreValueB64Writer, Visibility,
};
use rosenpass_util::functional::mutating;
use std::borrow::{Borrow, BorrowMut};
use std::fmt;
use std::io::Write;
use std::ops::{Deref, DerefMut};
use std::path::Path;
@@ -110,3 +116,164 @@ impl<const N: usize> StoreValue for Public<N> {
Ok(())
}
}
impl<const N: usize> LoadValueB64 for Public<N> {
type Error = anyhow::Error;
fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
where
Self: Sized,
{
let mut f = [0u8; F];
let mut v = Public::zero();
let p = path.as_ref();
let len = fopen_r(p)?
.read_slice_to_end(&mut f)
.with_context(|| format!("Could not load file {p:?}"))?;
b64_decode(&f[0..len], &mut v.value)
.with_context(|| format!("Could not decode base64 file {p:?}"))?;
Ok(v)
}
}
impl<const N: usize> StoreValueB64 for Public<N> {
type Error = anyhow::Error;
fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
let p = path.as_ref();
let mut f = [0u8; F];
let encoded_str = b64_encode(&self.value, &mut f)
.with_context(|| format!("Could not encode base64 file {p:?}"))?;
fopen_w(p, Visibility::Public)?
.write_all(encoded_str.as_bytes())
.with_context(|| format!("Could not write file {p:?}"))?;
Ok(())
}
}
impl<const N: usize> StoreValueB64Writer for Public<N> {
type Error = anyhow::Error;
fn store_b64_writer<const F: usize, W: std::io::Write>(
&self,
mut writer: W,
) -> Result<(), Self::Error> {
let mut f = [0u8; F];
let encoded_str = b64_encode(&self.value, &mut f)
.with_context(|| format!("Could not encode secret to base64"))?;
writer
.write_all(encoded_str.as_bytes())
.with_context(|| format!("Could not write base64 to writer"))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
#[cfg(test)]
mod tests {
use crate::Public;
use rosenpass_util::{
b64::b64_encode,
file::{
fopen_w, LoadValue, LoadValueB64, StoreValue, StoreValueB64, StoreValueB64Writer,
Visibility,
},
};
use std::{fs, os::unix::fs::PermissionsExt};
use tempfile::tempdir;
/// test loading a public from an example file, and then storing it again in a different file
#[test]
fn test_public_load_store() {
const N: usize = 100;
// Generate original random bytes
let original_bytes: [u8; N] = [rand::random(); N];
// Create a temporary directory
let temp_dir = tempdir().unwrap();
// Store the original public to an example file in the temporary directory
let example_file = temp_dir.path().join("example_file");
std::fs::write(example_file.clone(), &original_bytes).unwrap();
// Load the public from the example file
let loaded_public = Public::load(&example_file).unwrap();
// Check that the loaded public matches the original bytes
assert_eq!(&loaded_public.value, &original_bytes);
// Store the loaded public to a different file in the temporary directory
let new_file = temp_dir.path().join("new_file");
loaded_public.store(&new_file).unwrap();
// Read the contents of the new file
let new_file_contents = fs::read(&new_file).unwrap();
// Read the contents of the original file
let original_file_contents = fs::read(&example_file).unwrap();
// Check that the contents of the new file match the original file
assert_eq!(new_file_contents, original_file_contents);
}
/// test loading a base64 encoded public from an example file, and then storing it again in a different file
#[test]
fn test_public_load_store_base64() {
const N: usize = 100;
// Generate original random bytes
let original_bytes: [u8; N] = [rand::random(); N];
// Create a temporary directory
let temp_dir = tempdir().unwrap();
let example_file = temp_dir.path().join("example_file");
let mut encoded_public = [0u8; N * 2];
let encoded_public = b64_encode(&original_bytes, &mut encoded_public).unwrap();
std::fs::write(&example_file, encoded_public).unwrap();
// Load the public from the example file
let loaded_public = Public::load_b64::<{ N * 2 }, _>(&example_file).unwrap();
// Check that the loaded public matches the original bytes
assert_eq!(&loaded_public.value, &original_bytes);
// Store the loaded public to a different file in the temporary directory
let new_file = temp_dir.path().join("new_file");
loaded_public.store_b64::<{ N * 2 }, _>(&new_file).unwrap();
// Read the contents of the new file
let new_file_contents = fs::read(&new_file).unwrap();
// Read the contents of the original file
let original_file_contents = fs::read(&example_file).unwrap();
// Check that the contents of the new file match the original file
assert_eq!(new_file_contents, original_file_contents);
//Check new file permissions are public
let metadata = fs::metadata(&new_file).unwrap();
assert_eq!(metadata.permissions().mode() & 0o000777, 0o644);
// Store the loaded public to a different file in the temporary directory for a second time
let new_file = temp_dir.path().join("new_file_writer");
let new_file_writer = fopen_w(new_file.clone(), Visibility::Public).unwrap();
loaded_public
.store_b64_writer::<{ N * 2 }, _>(&new_file_writer)
.unwrap();
// Read the contents of the new file
let new_file_contents = fs::read(&new_file).unwrap();
// Read the contents of the original file
let original_file_contents = fs::read(&example_file).unwrap();
// Check that the contents of the new file match the original file
assert_eq!(new_file_contents, original_file_contents);
//Check new file permissions are public
let metadata = fs::metadata(&new_file).unwrap();
assert_eq!(metadata.permissions().mode() & 0o000777, 0o644);
}
}
}

View File

@@ -9,8 +9,11 @@ use anyhow::Context;
use rand::{Fill as Randomize, Rng};
use zeroize::{Zeroize, ZeroizeOnDrop};
use rosenpass_util::b64::b64_reader;
use rosenpass_util::file::{fopen_r, LoadValue, LoadValueB64, ReadExactToEnd};
use rosenpass_util::b64::{b64_decode, b64_encode};
use rosenpass_util::file::{
fopen_r, LoadValue, LoadValueB64, ReadExactToEnd, ReadSliceToEnd, StoreValueB64,
StoreValueB64Writer,
};
use rosenpass_util::functional::mutating;
use crate::alloc::{secret_box, SecretBox, SecretVec};
@@ -251,25 +254,57 @@ impl<const N: usize> LoadValue for Secret<N> {
impl<const N: usize> LoadValueB64 for Secret<N> {
type Error = anyhow::Error;
fn load_b64<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
use std::io::Read;
fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
let mut f: Secret<F> = Secret::random();
let mut v = Self::random();
let p = path.as_ref();
// This might leave some fragments of the secret on the stack;
// in practice this is likely not a problem because the stack likely
// will be overwritten by something else soon but this is not exactly
// guaranteed. It would be possible to remedy this, but since the secret
// data will linger in the Linux page cache anyways with the current
// implementation, going to great length to erase the secret here is
// not worth it right now.
b64_reader(&mut fopen_r(p)?)
.read_exact(v.secret_mut())
.with_context(|| format!("Could not load base64 file {p:?}"))?;
let len = fopen_r(p)?
.read_slice_to_end(f.secret_mut())
.with_context(|| format!("Could not load file {p:?}"))?;
b64_decode(&f.secret()[0..len], v.secret_mut())
.with_context(|| format!("Could not decode base64 file {p:?}"))?;
Ok(v)
}
}
impl<const N: usize> StoreValueB64 for Secret<N> {
type Error = anyhow::Error;
fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
let p = path.as_ref();
let mut f: Secret<F> = Secret::random();
let encoded_str = b64_encode(self.secret(), f.secret_mut())
.with_context(|| format!("Could not encode base64 file {p:?}"))?;
fopen_w(p, Visibility::Secret)?
.write_all(encoded_str.as_bytes())
.with_context(|| format!("Could not write file {p:?}"))?;
f.zeroize();
Ok(())
}
}
impl<const N: usize> StoreValueB64Writer for Secret<N> {
type Error = anyhow::Error;
fn store_b64_writer<const F: usize, W: Write>(&self, mut writer: W) -> anyhow::Result<()> {
let mut f: Secret<F> = Secret::random();
let encoded_str = b64_encode(self.secret(), f.secret_mut())
.with_context(|| format!("Could not encode secret to base64"))?;
writer
.write_all(encoded_str.as_bytes())
.with_context(|| format!("Could not write base64 to writer"))?;
f.zeroize();
Ok(())
}
}
impl<const N: usize> StoreSecret for Secret<N> {
type Error = anyhow::Error;
@@ -287,6 +322,8 @@ impl<const N: usize> StoreSecret for Secret<N> {
#[cfg(test)]
mod test {
use super::*;
use std::{fs, os::unix::fs::PermissionsExt};
use tempfile::tempdir;
/// check that we can alloc using the magic pool
#[test]
@@ -297,7 +334,7 @@ mod test {
assert_eq!(secret.as_ref(), &[0; N]);
}
/// check that a secrete lives, even if its [SecretMemoryPool] is deleted
/// check that a secret lives, even if its [SecretMemoryPool] is deleted
#[test]
fn secret_memory_pool_drop() {
const N: usize = 0x100;
@@ -307,7 +344,7 @@ mod test {
assert_eq!(secret.as_ref(), &[0; N]);
}
/// check that a secrete can be reborn, freshly initialized with zero
/// check that a secret can be reborn, freshly initialized with zero
#[test]
fn secret_memory_pool_release() {
const N: usize = 1;
@@ -325,4 +362,92 @@ mod test {
// and that the secret was zeroized
assert_eq!(new_secret.as_ref(), &[0; N]);
}
/// test loading a secret from an example file, and then storing it again in a different file
#[test]
fn test_secret_load_store() {
const N: usize = 100;
// Generate original random bytes
let original_bytes: [u8; N] = [rand::random(); N];
// Create a temporary directory
let temp_dir = tempdir().unwrap();
// Store the original secret to an example file in the temporary directory
let example_file = temp_dir.path().join("example_file");
std::fs::write(example_file.clone(), &original_bytes).unwrap();
// Load the secret from the example file
let loaded_secret = Secret::load(&example_file).unwrap();
// Check that the loaded secret matches the original bytes
assert_eq!(loaded_secret.secret(), &original_bytes);
// Store the loaded secret to a different file in the temporary directory
let new_file = temp_dir.path().join("new_file");
loaded_secret.store(&new_file).unwrap();
// Read the contents of the new file
let new_file_contents = fs::read(&new_file).unwrap();
// Read the contents of the original file
let original_file_contents = fs::read(&example_file).unwrap();
// Check that the contents of the new file match the original file
assert_eq!(new_file_contents, original_file_contents);
}
/// test loading a base64 encoded secret from an example file, and then storing it again in a different file
#[test]
fn test_secret_load_store_base64() {
const N: usize = 100;
// Generate original random bytes
let original_bytes: [u8; N] = [rand::random(); N];
// Create a temporary directory
let temp_dir = tempdir().unwrap();
let example_file = temp_dir.path().join("example_file");
let mut encoded_secret = [0u8; N * 2];
let encoded_secret = b64_encode(&original_bytes, &mut encoded_secret).unwrap();
std::fs::write(&example_file, encoded_secret).unwrap();
// Load the secret from the example file
let loaded_secret = Secret::load_b64::<{ N * 2 }, _>(&example_file).unwrap();
// Check that the loaded secret matches the original bytes
assert_eq!(loaded_secret.secret(), &original_bytes);
// Store the loaded secret to a different file in the temporary directory
let new_file = temp_dir.path().join("new_file");
loaded_secret.store_b64::<{ N * 2 }, _>(&new_file).unwrap();
// Read the contents of the new file
let new_file_contents = fs::read(&new_file).unwrap();
// Read the contents of the original file
let original_file_contents = fs::read(&example_file).unwrap();
// Check that the contents of the new file match the original file
assert_eq!(new_file_contents, original_file_contents);
//Check new file permissions are secret
let metadata = fs::metadata(&new_file).unwrap();
assert_eq!(metadata.permissions().mode() & 0o000777, 0o600);
// Store the loaded secret to a different file in the temporary directory for a second time
let new_file = temp_dir.path().join("new_file_writer");
let new_file_writer = fopen_w(new_file.clone(), Visibility::Secret).unwrap();
loaded_secret
.store_b64_writer::<{ N * 2 }, _>(&new_file_writer)
.unwrap();
// Read the contents of the new file
let new_file_contents = fs::read(&new_file).unwrap();
// Read the contents of the original file
let original_file_contents = fs::read(&example_file).unwrap();
// Check that the contents of the new file match the original file
assert_eq!(new_file_contents, original_file_contents);
//Check new file permissions are secret
let metadata = fs::metadata(&new_file).unwrap();
assert_eq!(metadata.permissions().mode() & 0o000777, 0o600);
}
}

View File

@@ -12,7 +12,8 @@ readme = "readme.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = { workspace = true }
base64ct = { workspace = true }
anyhow = { workspace = true }
typenum = { workspace = true }
static_assertions = { workspace = true }
zeroize = {workspace = true}

View File

@@ -1,20 +1,130 @@
use base64::{
display::Base64Display as B64Display, read::DecoderReader as B64Reader,
write::EncoderWriter as B64Writer,
};
use std::io::{Read, Write};
use base64ct::{Base64, Decoder as B64Reader, Encoder as B64Writer};
use zeroize::Zeroize;
use base64::engine::general_purpose::GeneralPurpose as Base64Engine;
const B64ENGINE: Base64Engine = base64::engine::general_purpose::STANDARD;
use std::fmt::Display;
pub fn fmt_b64<'a>(payload: &'a [u8]) -> B64Display<'a, 'static, Base64Engine> {
B64Display::<'a, 'static>::new(payload, &B64ENGINE)
pub struct B64DisplayHelper<'a, const F: usize>(&'a [u8]);
impl<const F: usize> Display for B64DisplayHelper<'_, F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut bytes = [0u8; F];
let string = b64_encode(&self.0, &mut bytes).map_err(|_| std::fmt::Error)?;
let result = f.write_str(string);
bytes.zeroize();
result
}
}
pub fn b64_writer<W: Write>(w: W) -> B64Writer<'static, Base64Engine, W> {
B64Writer::new(w, &B64ENGINE)
pub trait B64Display {
fn fmt_b64<'o, const F: usize>(&'o self) -> B64DisplayHelper<'o, F>;
}
pub fn b64_reader<R: Read>(r: R) -> B64Reader<'static, Base64Engine, R> {
B64Reader::new(r, &B64ENGINE)
impl B64Display for [u8] {
fn fmt_b64<'o, const F: usize>(&'o self) -> B64DisplayHelper<'o, F> {
B64DisplayHelper(self)
}
}
impl<T: AsRef<[u8]>> B64Display for T {
fn fmt_b64<'o, const F: usize>(&'o self) -> B64DisplayHelper<'o, F> {
B64DisplayHelper(self.as_ref())
}
}
pub fn b64_decode(input: &[u8], output: &mut [u8]) -> anyhow::Result<()> {
let mut reader = B64Reader::<Base64>::new(input).map_err(|e| anyhow::anyhow!(e))?;
match reader.decode(output) {
Ok(_) => (),
Err(base64ct::Error::InvalidLength) => (),
Err(e) => {
return Err(anyhow::anyhow!(e));
}
}
if reader.is_finished() {
Ok(())
} else {
Err(anyhow::anyhow!(
"Input not decoded completely (buffer size too small?)"
))
}
}
pub fn b64_encode<'o>(input: &[u8], output: &'o mut [u8]) -> anyhow::Result<&'o str> {
let mut writer = B64Writer::<Base64>::new(output).map_err(|e| anyhow::anyhow!(e))?;
writer.encode(input).map_err(|e| anyhow::anyhow!(e))?;
writer.finish().map_err(|e| anyhow::anyhow!(e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_b64_encode() {
let input = b"Hello, World!";
let mut output = [0u8; 20];
let result = b64_encode(input, &mut output);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "SGVsbG8sIFdvcmxkIQ==");
}
#[test]
fn test_b64_encode_small_buffer() {
let input = b"Hello, World!";
let mut output = [0u8; 10]; // Small output buffer
let result = b64_encode(input, &mut output);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "invalid Base64 length");
}
#[test]
fn test_b64_encode_empty_buffer() {
let input = b"";
let mut output = [0u8; 16];
let result = b64_encode(input, &mut output);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "");
}
#[test]
fn test_b64_decode() {
let input = b"SGVsbG8sIFdvcmxkIQ==";
let mut output = [0u8; 1000];
b64_decode(input, &mut output).unwrap();
assert_eq!(&output[..13], b"Hello, World!");
}
#[test]
fn test_b64_decode_small_buffer() {
let input = b"SGVsbG8sIFdvcmxkIQ==";
let mut output = [0u8; 10]; // Small output buffer
let result = b64_decode(input, &mut output);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Input not decoded completely (buffer size too small?)"
);
}
#[test]
fn test_b64_decode_empty_buffer() {
let input = b"";
let mut output = [0u8; 16];
let result = b64_decode(input, &mut output);
assert!(result.is_err());
}
#[test]
fn test_fmt_b64() {
let input = b"Hello, World!";
let result = input.fmt_b64::<20>().to_string();
assert_eq!(result, "SGVsbG8sIFdvcmxkIQ==");
}
#[test]
fn test_fmt_b64_empty_input() {
let input = b"";
let result = input.fmt_b64::<16>().to_string();
assert_eq!(result, "");
}
}

View File

@@ -30,6 +30,30 @@ pub fn fopen_r<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
.open(path)
}
pub trait ReadSliceToEnd {
type Error;
fn read_slice_to_end(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error>;
}
impl<R: Read> ReadSliceToEnd for R {
type Error = anyhow::Error;
fn read_slice_to_end(&mut self, buf: &mut [u8]) -> anyhow::Result<usize> {
let mut dummy = [0u8; 8];
let mut read = 0;
while read < buf.len() {
let bytes_read = self.read(&mut buf[read..])?;
if bytes_read == 0 {
break;
}
read += bytes_read;
}
ensure!(self.read(&mut dummy)? == 0, "File too long!");
Ok(read)
}
}
pub trait ReadExactToEnd {
type Error;
@@ -58,13 +82,36 @@ pub trait LoadValue {
pub trait LoadValueB64 {
type Error;
fn load_b64<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
where
Self: Sized;
}
pub trait StoreValueB64 {
type Error;
fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>
where
Self: Sized;
}
pub trait StoreValueB64Writer {
type Error;
fn store_b64_writer<const F: usize, W: std::io::Write>(
&self,
writer: W,
) -> Result<(), Self::Error>;
}
pub trait StoreValue {
type Error;
fn store<P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>;
}
pub trait DisplayValueB64 {
type Error;
fn display_b64<'o>(&self, output: &'o mut [u8]) -> Result<&'o str, Self::Error>;
}