secstr-0.5.1/.cargo_vcs_info.json0000644000000001360000000000100123420ustar { "git": { "sha1": "e9a5ff34a1f1a6bde78364a4e7f45e3f3c1bf619" }, "path_in_vcs": "" }secstr-0.5.1/.gitignore000064400000000000000000000000241046102023000131160ustar 00000000000000/target/ Cargo.lock secstr-0.5.1/.rustfmt.toml000064400000000000000000000003211046102023000136050ustar 00000000000000max_width = 128 fn_call_width = 96 struct_lit_width = 64 struct_variant_width = 96 array_width = 256 newline_style = "Native" use_try_shorthand = true match_block_trailing_comma = true fn_call_style = "Block" secstr-0.5.1/.travis.yml000064400000000000000000000004311046102023000132410ustar 00000000000000jobs: include: - os: linux arch: amd64 - os: linux arch: arm64 - os: linux arch: ppc64le - os: osx - os: windows sudo: false language: rust script: cargo test --verbose && cargo test --verbose --features "serde" rust: - stable - nightly secstr-0.5.1/CODE_OF_CONDUCT.md000064400000000000000000000062321046102023000137340ustar 00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project owner at val@packett.cool. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project owner is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ secstr-0.5.1/Cargo.toml0000644000000024250000000000100103430ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "secstr" version = "0.5.1" authors = ["Val Packett "] description = "A data type suitable for storing sensitive information such as passwords and private keys in memory, featuring constant time equality, mlock and zeroing out." homepage = "https://codeberg.org/valpackett/secstr" documentation = "https://docs.rs/secstr/" readme = "README.md" keywords = [ "Security", "Memory", ] license = "Unlicense" repository = "https://codeberg.org/valpackett/secstr" [lib] name = "secstr" [dependencies.libc] version = "0.2" [dependencies.libsodium-sys] version = "0.2" optional = true [dependencies.pre] version = "=0.2" optional = true [dependencies.serde] version = "1.0" optional = true [dev-dependencies.pre] version = "=0.2" [dev-dependencies.serde_cbor] version = "0.11" secstr-0.5.1/Cargo.toml.orig000064400000000000000000000013601046102023000140210ustar 00000000000000[package] name = "secstr" version = "0.5.1" authors = ["Val Packett "] keywords = ["Security", "Memory"] description = "A data type suitable for storing sensitive information such as passwords and private keys in memory, featuring constant time equality, mlock and zeroing out." license = "Unlicense" readme = "README.md" homepage = "https://codeberg.org/valpackett/secstr" repository = "https://codeberg.org/valpackett/secstr" documentation = "https://docs.rs/secstr/" edition = "2018" [lib] name = "secstr" [dependencies] libc = "0.2" libsodium-sys = { version = "0.2", optional = true } pre = { version = "=0.2", optional = true } serde = { version = "1.0", optional = true } [dev-dependencies] pre = "=0.2" serde_cbor = "0.11" secstr-0.5.1/README.md000064400000000000000000000055771046102023000124270ustar 00000000000000[![crates.io](https://img.shields.io/crates/v/secstr.svg)](https://crates.io/crates/secstr) [![API Docs](https://docs.rs/secstr/badge.svg)](https://docs.rs/secstr/) [![Build Status](https://img.shields.io/travis/myfreeweb/secstr.svg?style=flat)](https://travis-ci.org/myfreeweb/secstr) [![unlicense](https://img.shields.io/badge/un-license-green.svg?style=flat)](http://unlicense.org) # secstr A [Rust] library that implements a data type (wrapper around `Vec`) suitable for storing sensitive information such as passwords and private keys in memory. Inspired by Haskell [securemem] and .NET [SecureString]. Featuring: - constant time comparison (does not short circuit on the first different character; but terminates instantly if strings have different length) - automatically zeroing out in the destructor - `mlock` and `madvise` protection if possible - formatting as `***SECRET***` to prevent leaking into logs - (optionally) using libsodium (through [sodiumoxide]'s [libsodium-sys]) for zeroing, comparison, and hashing (`std::hash::Hash`) - (optionally) de/serializable into anything [Serde] supports as a byte string - (optionally) compile-time checked [preconditions] for the public `unsafe` API [Rust]: https://www.rust-lang.org [securemem]: https://hackage.haskell.org/package/securemem [SecureString]: http://msdn.microsoft.com/en-us/library/system.security.securestring%28v=vs.110%29.aspx [sodiumoxide]: https://crates.io/crates/sodiumoxide [libsodium-sys]: https://crates.io/crates/libsodium-sys [Serde]: https://serde.rs/ [preconditions]: https://crates.io/crates/pre ## Usage ```rust extern crate secstr; use secstr::*; let pw = SecStr::from("correct horse battery staple"); // Compared in constant time: // (Obviously, you should store hashes in real apps, not plaintext passwords) let are_pws_equal = pw == SecStr::from("correct horse battery staple".to_string()); // true // Formatting, printing without leaking secrets into logs let text_to_print = format!("{}", SecStr::from("hello")); // "***SECRET***" // Clearing memory // THIS IS DONE AUTOMATICALLY IN THE DESTRUCTOR // (but you can force it) let mut my_sec = SecStr::from("hello"); my_sec.zero_out(); // (It also sets the length to 0) assert_eq!(my_sec.unsecure(), b""); ``` Be careful with `SecStr::from`: if you have a borrowed string, it will be copied. Use `SecStr::new` if you have a `Vec`. ## Contributing Please feel free to submit pull requests! By participating in this project you agree to follow the [Contributor Code of Conduct](http://contributor-covenant.org/version/1/4/) and to release your contributions under the Unlicense. [The list of contributors is available on GitHub](https://codeberg.org/valpackett/secstr/graphs/contributors). ## License This is free and unencumbered software released into the public domain. For more information, please refer to the `UNLICENSE` file or [unlicense.org](http://unlicense.org). secstr-0.5.1/UNLICENSE000064400000000000000000000022731046102023000124060ustar 00000000000000This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to secstr-0.5.1/src/lib.rs000064400000000000000000001202131046102023000130340ustar 00000000000000//! A data type suitable for storing sensitive information such as passwords and private keys in memory, featuring constant time equality, mlock and zeroing out. #[cfg(feature = "serde")] use serde::{ de::{self, Deserialize, Deserializer, Visitor}, ser::{Serialize, Serializer}, }; use std::{ borrow::{Borrow, BorrowMut}, fmt, str::FromStr, }; /** * Obtain the number of bytes stored in the given byte slice */ #[cfg(feature = "libsodium-sys")] fn size_of(slice: &[T]) -> usize { slice.len() * std::mem::size_of::() } /** * Create a slice reference from the given box reference */ #[cfg(feature = "libsodium-sys")] fn box_as_slice(reference: &Box) -> &[T] { std::slice::from_ref(reference) } #[cfg(feature = "libsodium-sys")] mod mem { extern crate libsodium_sys as sodium; use crate::size_of; use std; #[cfg_attr( any(test, feature = "pre"), pre::pre(valid_ptr(ptr, w)), pre::pre("`ptr` points to a single allocation that is valid for at least `count` bytes"), pre::pre(count <= std::isize::MAX as usize) )] pub unsafe fn zero(ptr: *mut u8, count: usize) { sodium::sodium_memzero(ptr as *mut _, count); } #[inline(never)] #[cfg_attr( any(test, feature = "pre"), pre::pre(valid_ptr(us, r)), pre::pre("`us` points to a single allocated object of initialized `u8` values that is valid for `us_len` bytes"), pre::pre(us_len <= isize::MAX as usize), pre::pre(valid_ptr(them, r)), pre::pre("`them` points to a single allocated object of initialized `u8` values that is valid for `them_len` bytes"), pre::pre(them_len <= isize::MAX as usize) )] pub unsafe fn cmp(us: *const u8, us_len: usize, them: *const u8, them_len: usize) -> bool { if us_len != them_len { return false; } sodium::sodium_memcmp(us as *const _, them as *const _, them_len) == 0 } #[cfg_attr(any(test, feature = "pre"), pre::pre)] pub fn hash(slice: &[T], state: &mut H) where H: std::hash::Hasher, { // Hash the private data let mut hash = [0u8; sodium::crypto_hash_BYTES as _]; unsafe { assert_eq!( sodium::crypto_hash(&mut hash[0] as *mut _, slice.as_ptr() as *const _, size_of(slice) as u64), 0 ); }; // Hash again with the current internal state of the outer hasher added as "salt" (will include a per-thread random value for the default SipHasher) let mut round2 = Vec::new(); { #[cfg_attr( any(test, feature = "pre"), forward(pre), assure( valid_ptr(data, r), reason = "the pointer is created from the reference `state`, which is guaranteed to be valid for reads" ), assure( proper_align(data), reason = "the type of the pointer is `u8` with alignment `1`, which every pointer is guaranteed to have" ), assure( "the allocated object at `data` is valid for `len * mem::size_of::()` bytes", reason = "since `mem::size_of::() == 1`, the allocated object must be valid for `len == mem::size_of::()` bytes, which it is guaranteed to be, because it is created from a reference to a value of `H`" ), assure( "the memory referenced by the returned slice is not mutated by any pointer for the duration of `'a`, except inside a contained `UnsafeCell`", reason = "the pointer is created from a mutable reference, so we have exlusive access to the value here and the returned slice is not modified either, so no pointer modifies the memory" ), assure( len * ::core::mem::size_of::() <= isize::MAX as usize, reason = "`len * mem::size_of::() == mem::size_of::()` and the compiler fails to compile types with a size greater than `isize::MAX`" ) )] let salt = unsafe { std::slice::from_raw_parts(state as *const H as *const u8, std::mem::size_of::()) }; round2.reserve_exact(hash.len() + salt.len()); round2.extend_from_slice(&hash); round2.extend_from_slice(salt); }; let mut hash2 = [0u8; sodium::crypto_hash_BYTES as _]; unsafe { assert_eq!(sodium::crypto_hash(&mut hash2[0] as *mut _, round2.as_ptr(), round2.len() as u64), 0); }; // Use this final value as state state.write(&hash2 as &[u8]); } } #[cfg(not(feature = "libsodium-sys"))] mod mem { use std; #[cfg_attr( any(test, feature = "pre"), pre::pre(valid_ptr(ptr, w)), pre::pre("`ptr` points to a single allocation that is valid for at least `count` bytes"), pre::pre(count <= std::isize::MAX as usize) )] #[inline(never)] pub unsafe fn zero(ptr: *mut u8, count: usize) { for i in 0..count { #[cfg_attr( any(test, feature = "pre"), forward(impl pre::std::mut_pointer), assure( "the starting and the resulting pointer are in bounds of the same allocated object", reason = "this is guaranteed by the precondition to this function" ), assure( "the computed offset, in bytes, does not overflow an `isize`", reason = "`computed offset <= count <= isize::MAX`" ), assure( "performing the offset does not result in overflow", reason = "a single allocation does not rely on overflow to index all elements and `i as isize >= 0`" ) )] let offset_ptr = ptr.offset(i as isize); #[cfg_attr( any(test, feature = "pre"), forward(pre), assure( valid_ptr(dst, w), reason = "the call to offset above produced a valid pointer into the allocation" ), assure( proper_align(dst), reason = "`align_of::<*mut u8>() == 1` and any pointer has an alignment of `1`" ) )] std::ptr::write_volatile(offset_ptr, 0); } } #[inline(never)] #[cfg_attr( any(test, feature = "pre"), pre::pre(valid_ptr(us, r)), pre::pre("`us` points to a single allocated object of initialized `u8` values that is valid for `us_len` bytes"), pre::pre(us_len <= isize::MAX as usize), pre::pre(valid_ptr(them, r)), pre::pre("`them` points to a single allocated object of initialized `u8` values that is valid for `them_len` bytes"), pre::pre(them_len <= isize::MAX as usize) )] pub unsafe fn cmp(us: *const u8, us_len: usize, them: *const u8, them_len: usize) -> bool { if us_len != them_len { return false; } let mut result: u8 = 0; for i in 0..us_len { let us_val = { #[cfg_attr( any(test, feature = "pre"), forward(impl pre::std::const_pointer), assure( "the starting and the resulting pointer are in bounds of the same allocated object", reason = "the offset is at most `us_len` bytes and the object at `us` is valid for `us_len` bytes" ), assure( "the computed offset, in bytes, does not overflow an `isize`", reason = "`us_len <= `isize::MAX as usize`" ), assure( "performing the offset does not result in overflow", reason = "a single allocation does not rely on overflow to index all elements and `i as isize >= 0`" ) )] let ptr = us.offset(i as isize); #[cfg_attr( any(test, feature = "pre"), forward(pre), assure( valid_ptr(src, r), reason = "`ptr` is constructed from a valid pointer above with an offset that still fits the allocation" ), assure( proper_align(src), reason = "`T` is `u8`, which has an alignment of `1`, which every pointer has" ), assure( "`src` points to a properly initialized value of type `T`", reason = "`ptr` points to the object as `us`, which contains initialized `u8` values" ), assure("`T` is `Copy` or the value at `*src` isn't used after this call", reason = "`u8: Copy`") )] std::ptr::read_volatile(ptr) }; let them_val = { #[cfg_attr( any(test, feature = "pre"), forward(impl pre::std::const_pointer), assure( "the starting and the resulting pointer are in bounds of the same allocated object", reason = "the offset is at most `them_len == us_len` bytes and the object at `them` is valid for `them_len` bytes" ), assure( "the computed offset, in bytes, does not overflow an `isize`", reason = "`them_len == us_len <= `isize::MAX as usize`" ), assure( "performing the offset does not result in overflow", reason = "a single allocation does not rely on overflow to index all elements and `i as isize >= 0`" ) )] let ptr = them.offset(i as isize); #[cfg_attr( any(test, feature = "pre"), forward(pre), assure( valid_ptr(src, r), reason = "`ptr` is constructed from a valid pointer above with an offset that still fits the allocation" ), assure( proper_align(src), reason = "`T` is `u8`, which has an alignment of `1`, which every pointer has" ), assure( "`src` points to a properly initialized value of type `T`", reason = "`ptr` points to the object as `them`, which contains initialized `u8` values" ), assure("`T` is `Copy` or the value at `*src` isn't used after this call", reason = "`u8: Copy`") )] std::ptr::read_volatile(ptr) }; result |= us_val ^ them_val; } result == 0 } } #[cfg(unix)] mod memlock { extern crate libc; pub fn mlock(cont: *mut T, count: usize) { let byte_num = count * std::mem::size_of::(); unsafe { let ptr = cont as *mut libc::c_void; libc::mlock(ptr, byte_num); #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] libc::madvise(ptr, byte_num, libc::MADV_NOCORE); #[cfg(target_os = "linux")] libc::madvise(ptr, byte_num, libc::MADV_DONTDUMP); } } pub fn munlock(cont: *mut T, count: usize) { let byte_num = count * std::mem::size_of::(); unsafe { let ptr = cont as *mut libc::c_void; libc::munlock(ptr, byte_num); #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] libc::madvise(ptr, byte_num, libc::MADV_CORE); #[cfg(target_os = "linux")] libc::madvise(ptr, byte_num, libc::MADV_DODUMP); } } } #[cfg(not(unix))] mod memlock { pub fn mlock(cont: *mut T, count: usize) {} pub fn munlock(cont: *mut T, count: usize) {} } mod private { // Private trait to prevent users from implementing `NoPaddingBytes` // This allows to change to a better implementation of `NoPaddingBytes` in the future, // without worrying about breaking backwards compatibility for users who implemented the trait. pub trait Sealed {} } /// Guarantees that there are no padding bytes in types implementing this trait. /// /// This trait is sealed and cannot be implemented outside of this crate. pub unsafe trait NoPaddingBytes: private::Sealed {} macro_rules! impl_no_padding_bytes { ($($type:ty),*) => { $( impl private::Sealed for $type {} unsafe impl NoPaddingBytes for $type {} )* }; } impl_no_padding_bytes! { u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize, char, f32, f64, () } impl private::Sealed for [T; N] {} unsafe impl NoPaddingBytes for [T; N] {} /// Type alias for a vector that stores just bytes pub type SecStr = SecVec; /// Wrapper for a vector that stores a valid UTF-8 string #[derive(Clone, Eq)] pub struct SecUtf8(SecVec); impl SecUtf8 { /// Borrow the contents of the string. #[cfg_attr(any(test, feature = "pre"), pre::pre)] pub fn unsecure(&self) -> &str { #[cfg_attr( any(test, feature = "pre"), forward(pre), assure( "the content of `v` is valid UTF-8", reason = "it is not possible to create a `SecUtf8` with invalid UTF-8 content and it is also not possible to modify the content as non-UTF-8 directly, so they must still be valid UTF-8 here" ) )] unsafe { std::str::from_utf8_unchecked(self.0.unsecure()) } } /// Mutably borrow the contents of the string. #[cfg_attr(any(test, feature = "pre"), pre::pre)] pub fn unsecure_mut(&mut self) -> &mut str { #[cfg_attr( any(test, feature = "pre"), forward(pre), assure( "the content of `v` is valid UTF-8", reason = "it is not possible to create a `SecUtf8` with invalid UTF-8 content and it is also not possible to modify the content as non-UTF-8 directly, so they must still be valid UTF-8 here" ) )] unsafe { std::str::from_utf8_unchecked_mut(self.0.unsecure_mut()) } } /// Turn the string into a regular `String` again. #[cfg_attr(any(test, feature = "pre"), pre::pre)] pub fn into_unsecure(mut self) -> String { memlock::munlock(self.0.content.as_mut_ptr(), self.0.content.capacity()); let content = std::mem::replace(&mut self.0.content, Vec::new()); std::mem::forget(self); #[cfg_attr( any(test, feature = "pre"), forward(impl pre::std::string::String), assure( "the content of `bytes` is valid UTF-8", reason = "it is not possible to create a `SecUtf8` with invalid UTF-8 content and it is also not possible to modify the content as non-UTF-8 directly, so they must still be valid UTF-8 here" ) )] unsafe { String::from_utf8_unchecked(content) } } } impl PartialEq for SecUtf8 { fn eq(&self, other: &SecUtf8) -> bool { // use implementation of SecVec self.0 == other.0 } } impl fmt::Debug for SecUtf8 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("***SECRET***").map_err(|_| fmt::Error) } } impl fmt::Display for SecUtf8 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("***SECRET***").map_err(|_| fmt::Error) } } impl From for SecUtf8 where U: Into, { fn from(s: U) -> SecUtf8 { SecUtf8(SecVec::new(s.into().into_bytes())) } } impl FromStr for SecUtf8 { type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { Ok(SecUtf8(SecVec::new(s.into()))) } } #[cfg(feature = "serde")] impl serde::Serialize for SecUtf8 { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(self.unsecure()) } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for SecUtf8 { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct SecUtf8Visitor; impl<'de> serde::de::Visitor<'de> for SecUtf8Visitor { type Value = SecUtf8; fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { write!(formatter, "an utf-8 encoded string") } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { Ok(SecUtf8::from(v.to_string())) } } deserializer.deserialize_string(SecUtf8Visitor) } } /// A data type suitable for storing sensitive information such as passwords and private keys in memory, that implements: /// /// - Automatic zeroing in `Drop` /// - Constant time comparison in `PartialEq` (does not short circuit on the first different character; but terminates instantly if strings have different length) /// - Outputting `***SECRET***` to prevent leaking secrets into logs in `fmt::Debug` and `fmt::Display` /// - Automatic `mlock` to protect against leaking into swap (any unix) /// - Automatic `madvise(MADV_NOCORE/MADV_DONTDUMP)` to protect against leaking into core dumps (FreeBSD, DragonflyBSD, Linux) /// /// Comparisons using the `PartialEq` implementation are undefined behavior (and most likely wrong) if `T` has any padding bytes. /// /// Be careful with `SecStr::from`: if you have a borrowed string, it will be copied. /// Use `SecStr::new` if you have a `Vec`. pub struct SecVec where T: Sized + Copy, { content: Vec, } impl SecVec where T: Sized + Copy, { pub fn new(mut cont: Vec) -> Self { memlock::mlock(cont.as_mut_ptr(), cont.capacity()); SecVec { content: cont } } /// Borrow the contents of the string. pub fn unsecure(&self) -> &[T] { self.borrow() } /// Mutably borrow the contents of the string. pub fn unsecure_mut(&mut self) -> &mut [T] { self.borrow_mut() } /// Resizes the `SecVec` in-place so that len is equal to `new_len`. /// /// If `new_len` is smaller the inner vector is truncated. /// If `new_len` is larger the inner vector will grow, placing `value` in all new cells. /// /// This ensures that the new memory region is secured if reallocation occurs. /// /// Similar to [`Vec::resize`](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.resize) pub fn resize(&mut self, new_len: usize, value: T) { // Trucnate if shorter or same length if new_len <= self.content.len() { self.content.truncate(new_len); return; } // Allocate new vector, copy old data into it let mut new_vec = vec![value; new_len]; memlock::mlock(new_vec.as_mut_ptr(), new_vec.capacity()); new_vec[0..self.content.len()].copy_from_slice(&self.content); // Securely clear old vector, replace with new vector self.zero_out(); memlock::munlock(self.content.as_mut_ptr(), self.content.capacity()); self.content = new_vec; } /// Overwrite the string with zeros. This is automatically called in the destructor. /// /// This also sets the length to `0`. #[cfg_attr(any(test, feature = "pre"), pre::pre)] pub fn zero_out(&mut self) { // We zero the entire capacity, not just the currently initialized capacity let num_bytes = self.content.capacity() * std::mem::size_of::(); // We can set the length to zero without worrying about dropping, because `T: Copy` and // `Copy` types cannot implement `Drop`. #[cfg_attr( any(test, feature = "pre"), forward(impl pre::std::vec::Vec), assure( new_len <= self.capacity(), reason = "`0` is smaller or equal to any `usize` and `self.capacity()` is a `usize`" ), assure( "the elements at `old_len..new_len` are initialized", reason = "`new_len <= old_len`, so `old_len..new_len` is an empty range" ) )] unsafe { self.content.set_len(0) }; #[cfg_attr( any(test, feature = "pre"), assure( valid_ptr(ptr, w), reason = "the vector is a valid pointer or has length zero and any pointer is valid for zero bytes" ), assure( "`ptr` points to a single allocation that is valid for at least `count` bytes", reason = "a vector always points to a single allocation and `count` is the size of the allocation in bytes" ), assure( count <= std::isize::MAX as usize, reason = "a vector never allocates more than `isize::MAX` elements" ) )] unsafe { mem::zero(self.content.as_mut_ptr() as *mut u8, num_bytes) }; } } impl Clone for SecVec { fn clone(&self) -> Self { Self::new(self.content.clone()) } } // Creation impl From for SecVec where U: Into>, T: Sized + Copy, { fn from(s: U) -> SecVec { SecVec::new(s.into()) } } impl FromStr for SecVec { type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { Ok(SecVec::new(s.into())) } } // Vec item indexing impl std::ops::Index for SecVec where T: Sized + Copy, Vec: std::ops::Index, { type Output = as std::ops::Index>::Output; fn index(&self, index: U) -> &Self::Output { std::ops::Index::index(&self.content, index) } } // Borrowing impl Borrow<[T]> for SecVec where T: Sized + Copy, { fn borrow(&self) -> &[T] { self.content.borrow() } } impl BorrowMut<[T]> for SecVec where T: Sized + Copy, { fn borrow_mut(&mut self) -> &mut [T] { self.content.borrow_mut() } } // Overwrite memory with zeros when we're done impl Drop for SecVec where T: Sized + Copy, { fn drop(&mut self) { self.zero_out(); memlock::munlock(self.content.as_mut_ptr(), self.content.capacity()); } } // Constant time comparison impl PartialEq for SecVec where T: Sized + Copy + NoPaddingBytes, { #[cfg_attr(any(test, feature = "pre"), pre::pre)] fn eq(&self, other: &SecVec) -> bool { #[cfg_attr( any(test, feature = "pre"), assure( valid_ptr(us, r), reason = "`us` is created from a reference" ), assure( "`us` points to a single allocated object of initialized `u8` values that is valid for `us_len` bytes", reason = "`T` has no padding bytes, because of the `NoPaddingBytes` bound and all other bytes are initialized, because all elements in a vec are initialized. They also all belong to a single allocation big enough to hold at least `vec.len()` elements of `T`." ), assure( us_len <= isize::MAX as usize, reason = "a slice is never larger than `isize::MAX` bytes" ), assure( valid_ptr(them, r), reason = "`them` is created from a reference" ), assure( "`them` points to a single allocated object of initialized `u8` values that is valid for `them_len` bytes", reason = "`T` has no padding bytes, because of the `NoPaddingBytes` bound and all other bytes are initialized, because all elements in a vec are initialized. They also all belong to a single allocation big enough to hold at least `vec.len()` elements of `T`." ), assure( them_len <= isize::MAX as usize, reason = "a slice is never larger than `isize::MAX` bytes" ) )] unsafe { mem::cmp( self.content.as_ptr() as *const u8, self.content.len() * std::mem::size_of::(), other.content.as_ptr() as *const u8, other.content.len() * std::mem::size_of::(), ) } } } impl Eq for SecVec where T: Sized + Copy + NoPaddingBytes {} // Make sure sensitive information is not logged accidentally impl fmt::Debug for SecVec where T: Sized + Copy, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("***SECRET***").map_err(|_| fmt::Error) } } impl fmt::Display for SecVec where T: Sized + Copy, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("***SECRET***").map_err(|_| fmt::Error) } } #[cfg(feature = "serde")] struct BytesVisitor; #[cfg(feature = "serde")] impl<'de> Visitor<'de> for BytesVisitor { type Value = SecVec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a byte array") } fn visit_bytes(self, value: &[u8]) -> Result, E> where E: de::Error, { Ok(SecStr::from(value)) } } #[cfg(feature = "serde")] impl<'de> Deserialize<'de> for SecVec { fn deserialize(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_bytes(BytesVisitor) } } #[cfg(feature = "libsodium-sys")] impl std::hash::Hash for SecVec where T: Sized + Copy, { fn hash(&self, state: &mut H) where H: std::hash::Hasher, { mem::hash(&self.content, state); } } #[cfg(feature = "serde")] impl Serialize for SecVec { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_bytes(self.content.borrow()) } } /// A data type suitable for storing sensitive information such as passwords and private keys in memory, that implements: /// /// - Automatic zeroing in `Drop` /// - Constant time comparison in `PartialEq` (does not short circuit on the first different character; but terminates instantly if strings have different length) /// - Outputting `***SECRET***` to prevent leaking secrets into logs in `fmt::Debug` and `fmt::Display` /// - Automatic `mlock` to protect against leaking into swap (any unix) /// - Automatic `madvise(MADV_NOCORE/MADV_DONTDUMP)` to protect against leaking into core dumps (FreeBSD, DragonflyBSD, Linux) /// /// Comparisons using the `PartialEq` implementation are undefined behavior (and most likely wrong) if `T` has any padding bytes. pub struct SecBox where T: Sized + Copy, { // This is an `Option` to avoid UB in the destructor, outside the destructor, it is always // `Some(_)` content: Option>, } impl SecBox where T: Sized + Copy, { pub fn new(mut cont: Box) -> Self { memlock::mlock(&mut cont, std::mem::size_of::()); SecBox { content: Some(cont) } } /// Borrow the contents of the string. pub fn unsecure(&self) -> &T { self.content.as_ref().unwrap() } /// Mutably borrow the contents of the string. pub fn unsecure_mut(&mut self) -> &mut T { self.content.as_mut().unwrap() } } impl Clone for SecBox { fn clone(&self) -> Self { Self::new(self.content.clone().unwrap()) } } /// Overwrite the contents with zeros. This is automatically done in the destructor. /// /// # Safety /// An all-zero byte-pattern must be a valid value of `T` in order for this function call to not be /// undefined behavior. #[cfg_attr(any(test, feature = "pre"), pre::pre("an all-zero byte-pattern is a valid value of `T`"))] pub unsafe fn zero_out_secbox(secbox: &mut SecBox) where T: Sized + Copy, { #[cfg_attr( any(test, feature = "pre"), assure( valid_ptr(ptr, w), reason = "`ptr` comes from a valid box, which is guaranteed to be a valid pointer" ), assure( "`ptr` points to a single allocation that is valid for at least `count` bytes", reason = "a `Box` points to an allocation of at least `mem::size_of::()` bytes" ), assure( count <= std::isize::MAX as usize, reason = "`mem::size_of::()` cannot return a value larger than `isize::MAX`" ) )] mem::zero(&mut **secbox.content.as_mut().unwrap() as *mut T as *mut u8, std::mem::size_of::()); } // Delegate indexing impl std::ops::Index for SecBox where T: std::ops::Index + Sized + Copy, { type Output = >::Output; fn index(&self, index: U) -> &Self::Output { std::ops::Index::index(self.content.as_ref().unwrap().as_ref(), index) } } // Borrowing impl Borrow for SecBox where T: Sized + Copy, { fn borrow(&self) -> &T { self.content.as_ref().unwrap() } } impl BorrowMut for SecBox where T: Sized + Copy, { fn borrow_mut(&mut self) -> &mut T { self.content.as_mut().unwrap() } } // Overwrite memory with zeros when we're done impl Drop for SecBox where T: Sized + Copy, { #[cfg_attr(any(test, feature = "pre"), pre::pre)] fn drop(&mut self) { // Make sure that the box does not need to be dropped after this function, because it may // see an invalid type, if `T` does not support an all-zero byte-pattern // Instead we manually destruct the box and only handle the potentially invalid values // behind the pointer let ptr = Box::into_raw(self.content.take().unwrap()); // There is no need to worry about dropping the contents, because `T: Copy` and `Copy` // types cannot implement `Drop` #[cfg_attr( any(test, feature = "pre"), assure( valid_ptr(ptr, w), reason = "`ptr` comes from a valid box, which is guaranteed to be a valid pointer" ), assure( "`ptr` points to a single allocation that is valid for at least `count` bytes", reason = "a `Box` points to an allocation of at least `mem::size_of::()` bytes" ), assure( count <= std::isize::MAX as usize, reason = "`mem::size_of::()` cannot return a value larger than `isize::MAX`" ) )] unsafe { mem::zero(ptr as *mut u8, std::mem::size_of::()) }; memlock::munlock(ptr, std::mem::size_of::()); // Deallocate only non-zero-sized types, because otherwise it's UB if std::mem::size_of::() != 0 { // Safety: // This way to manually deallocate is advertised in the documentation of `Box::into_raw`. // The box was allocated with the global allocator and a layout of `T` and is thus // deallocated using the same allocator and layout here. unsafe { std::alloc::dealloc(ptr as *mut u8, std::alloc::Layout::new::()) }; } } } // Constant time comparison impl PartialEq for SecBox where T: Sized + Copy + NoPaddingBytes, { #[cfg_attr(any(test, feature = "pre"), pre::pre)] fn eq(&self, other: &SecBox) -> bool { #[cfg_attr( any(test, feature = "pre"), assure( valid_ptr(us, r), reason = "`us` is created from a reference" ), assure( "`us` points to a single allocated object of initialized `u8` values that is valid for `us_len` bytes", reason = "`T` has no padding bytes, because of the `NoPaddingBytes` bound and all other bytes are initialized, because all elements in a vec are initialized. They also all belong to a single allocation big enough to hold at least `vec.len()` elements of `T`." ), assure( us_len <= isize::MAX as usize, reason = "`mem::size_of::()` is never larger than `isize::MAX` bytes" ), assure( valid_ptr(them, r), reason = "`them` is created from a reference" ), assure( "`them` points to a single allocated object of initialized `u8` values that is valid for `them_len` bytes", reason = "`T` has no padding bytes, because of the `NoPaddingBytes` bound and all other bytes are initialized, because all elements in a vec are initialized. They also all belong to a single allocation big enough to hold at least `vec.len()` elements of `T`." ), assure( them_len <= isize::MAX as usize, reason = "`mem::size_of::()` is never larger than `isize::MAX` bytes" ) )] unsafe { mem::cmp( &**self.content.as_ref().unwrap() as *const T as *const u8, std::mem::size_of::(), &**other.content.as_ref().unwrap() as *const T as *const u8, std::mem::size_of::(), ) } } } impl Eq for SecBox where T: Sized + Copy + NoPaddingBytes {} // Make sure sensitive information is not logged accidentally impl fmt::Debug for SecBox where T: Sized + Copy, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("***SECRET***").map_err(|_| fmt::Error) } } impl fmt::Display for SecBox where T: Sized + Copy, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("***SECRET***").map_err(|_| fmt::Error) } } #[cfg(feature = "libsodium-sys")] impl std::hash::Hash for SecBox where T: Sized + Copy, { fn hash(&self, state: &mut H) where H: std::hash::Hasher, { mem::hash(box_as_slice(self.content.as_ref().unwrap()), state); } } #[cfg(test)] mod tests { use super::{zero_out_secbox, SecBox, SecStr, SecVec}; #[test] fn test_basic() { let my_sec = SecStr::from("hello"); assert_eq!(my_sec, SecStr::from("hello".to_string())); assert_eq!(my_sec.unsecure(), b"hello"); } #[test] #[cfg_attr(any(test, feature = "pre"), pre::pre)] fn test_zero_out() { let mut my_sec = SecStr::from("hello"); my_sec.zero_out(); // `zero_out` sets the `len` to 0, here we reset it to check that the bytes were zeroed #[cfg_attr( any(test, feature = "pre"), forward(impl pre::std::vec::Vec), assure( new_len <= self.capacity(), reason = "the call to `zero_out` did not reduce the capacity and the length was `5` before, so the capacity must be greater or equal to `5`" ), assure( "the elements at `old_len..new_len` are initialized", reason = "they were initialized to `0` by the call to `zero_out`" ) )] unsafe { my_sec.content.set_len(5) } assert_eq!(my_sec.unsecure(), b"\x00\x00\x00\x00\x00"); } #[test] fn test_resize() { let mut my_sec = SecVec::from([0, 1]); assert_eq!(my_sec.unsecure().len(), 2); my_sec.resize(1, 0); assert_eq!(my_sec.unsecure().len(), 1); my_sec.resize(16, 2); assert_eq!(my_sec.unsecure(), &[0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]); } #[test] fn test_comparison() { assert_eq!(SecStr::from("hello"), SecStr::from("hello")); assert!(SecStr::from("hello") != SecStr::from("yolo")); assert!(SecStr::from("hello") != SecStr::from("olleh")); assert!(SecStr::from("hello") != SecStr::from("helloworld")); assert!(SecStr::from("hello") != SecStr::from("")); } #[test] fn test_indexing() { let string = SecStr::from("hello"); assert_eq!(string[0], 'h' as u8); assert_eq!(&string[3..5], "lo".as_bytes()); } #[test] fn test_show() { assert_eq!(format!("{:?}", SecStr::from("hello")), "***SECRET***".to_string()); assert_eq!(format!("{}", SecStr::from("hello")), "***SECRET***".to_string()); } #[cfg(feature = "libsodium-sys")] #[test] fn test_hashing() { use std::hash::*; let value = SecStr::from("hello"); let mut hasher = SipHasher::new(); // Variant of SipHasher that does not use random values value.hash(&mut hasher); assert_eq!(hasher.finish(), 12960579610752219549); } #[test] #[cfg_attr(any(test, feature = "pre"), pre::pre)] fn test_comparison_zero_out_mb() { let mbstring1 = SecVec::from(vec!['H', 'a', 'l', 'l', 'o', ' ', '🦄', '!']); let mbstring2 = SecVec::from(vec!['H', 'a', 'l', 'l', 'o', ' ', '🦄', '!']); let mbstring3 = SecVec::from(vec!['!', '🦄', ' ', 'o', 'l', 'l', 'a', 'H']); assert!(mbstring1 == mbstring2); assert!(mbstring1 != mbstring3); let mut mbstring = mbstring1.clone(); mbstring.zero_out(); // `zero_out` sets the `len` to 0, here we reset it to check that the bytes were zeroed #[cfg_attr( any(test, feature = "pre"), forward(impl pre::std::vec::Vec), assure( new_len <= self.capacity(), reason = "the call to `zero_out` did not reduce the capacity and the length was `8` before, so the capacity must be greater or equal to `8`" ), assure( "the elements at `old_len..new_len` are initialized", reason = "they were initialized to `0` by the call to `zero_out`" ) )] unsafe { mbstring.content.set_len(8) } assert_eq!(mbstring.unsecure(), &['\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0']); } const PRIVATE_KEY_1: [u8; 32] = [ 0xb0, 0x3b, 0x34, 0xc3, 0x3a, 0x1c, 0x44, 0xf2, 0x25, 0xb6, 0x62, 0xd2, 0xbf, 0x48, 0x59, 0xb8, 0x13, 0x54, 0x11, 0xfa, 0x7b, 0x03, 0x86, 0xd4, 0x5f, 0xb7, 0x5d, 0xc5, 0xb9, 0x1b, 0x44, 0x66, ]; const PRIVATE_KEY_2: [u8; 32] = [ 0xc8, 0x06, 0x43, 0x9d, 0xc9, 0xd2, 0xc4, 0x76, 0xff, 0xed, 0x8f, 0x25, 0x80, 0xc0, 0x88, 0x8d, 0x58, 0xab, 0x40, 0x6b, 0xf7, 0xae, 0x36, 0x98, 0x87, 0x90, 0x21, 0xb9, 0x6b, 0xb4, 0xbf, 0x59, ]; #[test] #[cfg_attr(any(test, feature = "pre"), pre::pre)] fn test_secbox() { let key_1 = SecBox::new(Box::new(PRIVATE_KEY_1)); let key_2 = SecBox::new(Box::new(PRIVATE_KEY_2)); let key_3 = SecBox::new(Box::new(PRIVATE_KEY_1)); assert!(key_1 == key_1); assert!(key_1 != key_2); assert!(key_2 != key_3); assert!(key_1 == key_3); let mut final_key = key_1.clone(); #[cfg_attr( any(test, feature = "pre"), assure( "an all-zero byte-pattern is a valid value of `T`", reason = "`T` is `i32`, for which an all-zero byte-pattern is valid" ) )] unsafe { zero_out_secbox(&mut final_key) }; assert_eq!(final_key.unsecure(), &[0; 32]); } #[cfg(feature = "serde")] #[test] fn test_serialization() { use serde_cbor::{from_slice, to_vec}; let my_sec = SecStr::from("hello"); let my_cbor = to_vec(&my_sec).unwrap(); assert_eq!(my_cbor, b"\x45hello"); let my_sec2 = from_slice(&my_cbor).unwrap(); assert_eq!(my_sec, my_sec2); } }