Skip to content

Commit

Permalink
support keydb feature
Browse files Browse the repository at this point in the history
This adds a feature `keydb` to rc_crypto's nss create, which enables the
`ensure_initialized_with_profile_dir` initialize function. This
configures NSS to use a profile and persist keys into key4.db.

Also adding methods for managing AES256 keys with NSS:
* `authentication_with_primary_password_is_needed`: check wheather primary password is enabled
* `authenticate_with_primary_password`: authenticate with primary password against NSS key database
* `get_or_create_aes256_key`: retrieve a key from key4.db or, if not present, create one
  • Loading branch information
jo committed Jan 10, 2025
1 parent 5e3fb6c commit aac3e37
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

### `rc_crypto`
- New low level bindings for dealing with primary password.
- New feature flag `keydb` in `rc_crypto/nss`, which enables NSS key persistence: `ensure_initialized_with_profile_dir(path: impl AsRef<Path>)` initializes NSS with a profile directory and appropriate flags to persist keys (and certificates) in its internal PKCS11 software implementation. This function must be called first; if `ensure_initialized` is called before, it will fail.
- New methods for dealing with primary password and key persistence, available within the `keydb` feature:
* `authentication_with_primary_password_is_needed()`: checks whether a primary password is set and needs to be authenticated
* `authenticate_with_primary_password(primary_password: &str)`: method for authenticate NSS key store against a user-provided primary password
* `get_or_create_aes256_key(name: &str)`: retrieve a key by `name` from the internal NSS key store. If none exists, create one, persist, and return.

[Full Changelog](In progress)

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions components/support/rc_crypto/nss/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ error-support = { path = "../../error" }
nss_sys = { path = "nss_sys" }
serde = "1"
serde_derive = "1"
once_cell = { version = "1.20.2", optional = true }

[features]
default = []
gecko = ["nss_sys/gecko"]
keydb = ["dep:once_cell"]
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extern "C" {
pub fn PK11_GetInternalKeySlot() -> *mut PK11SlotInfo;
pub fn PK11_NeedUserInit(slot: *mut PK11SlotInfo) -> PRBool;
pub fn PK11_NeedLogin(slot: *mut PK11SlotInfo) -> PRBool;
pub fn PK11_IsLoggedIn(slot: *mut PK11SlotInfo, wincx: *mut c_void) -> PRBool;
pub fn PK11_CheckUserPassword(slot: *mut PK11SlotInfo, password: *const c_char) -> SECStatus;
pub fn PK11_GenerateRandom(data: *mut c_uchar, len: c_int) -> SECStatus;
pub fn PK11_FreeSymKey(key: *mut PK11SymKey);
Expand Down
8 changes: 6 additions & 2 deletions components/support/rc_crypto/nss/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

#[derive(Debug, thiserror::Error)]
pub enum ErrorKind {
#[error("NSS could not be initialized")]
NSSInitFailure,
#[error("NSS could not be initialized: {0}")]
NSSInitFailure(String),
#[error("NSS error: {0} {1}")]
NSSError(i32, String),
#[error("SSL error: {0} {1}")]
Expand All @@ -16,6 +16,10 @@ pub enum ErrorKind {
InputError(String),
#[error("Internal crypto error")]
InternalError,
#[error("invalid key length")]
InvalidKeyLength,
#[error("Interior nul byte was found")]
NulError,
#[error("Conversion error: {0}")]
ConversionError(#[from] std::num::TryFromIntError),
#[error("Base64 decode error: {0}")]
Expand Down
3 changes: 3 additions & 0 deletions components/support/rc_crypto/nss/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ pub mod pkixc;
pub mod secport;
pub use crate::error::{Error, ErrorKind, Result};
pub use util::ensure_nss_initialized as ensure_initialized;

#[cfg(feature = "keydb")]
pub use util::ensure_nss_initialized_with_profile_dir as ensure_initialized_with_profile_dir;
8 changes: 8 additions & 0 deletions components/support/rc_crypto/nss/src/pk11/slot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ pub fn generate_random(data: &mut [u8]) -> Result<()> {
pub(crate) fn get_internal_slot() -> Result<Slot> {
unsafe { Slot::from_ptr(nss_sys::PK11_GetInternalSlot()) }
}

/// Safe wrapper around `PK11_GetInternalKeySlot` that
/// de-allocates memory when the slot goes out of
/// scope.
#[cfg(feature = "keydb")]
pub(crate) fn get_internal_key_slot() -> Result<Slot> {
unsafe { Slot::from_ptr(nss_sys::PK11_GetInternalKeySlot()) }
}
198 changes: 196 additions & 2 deletions components/support/rc_crypto/nss/src/pk11/sym_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#[cfg(feature = "keydb")]
use crate::util::get_last_error;
use crate::{
error::*,
pk11::{context::HashAlgorithm, slot, types::SymKey},
util::{ensure_nss_initialized, map_nss_secstatus, sec_item_as_slice, ScopedPtr},
};
#[cfg(feature = "keydb")]
use std::ffi::{c_char, CString};
use std::{
mem,
os::raw::{c_uchar, c_uint, c_ulong},
Expand Down Expand Up @@ -54,13 +58,17 @@ pub fn hkdf_expand(
)?
};
map_nss_secstatus(|| unsafe { nss_sys::PK11_ExtractKeyValue(sym_key.as_mut_ptr()) })?;
// This doesn't leak, because the SECItem* returned by PK11_GetKeyData
// just refers to a buffer managed by `sym_key` which we copy into `out`.
let mut key_data = unsafe { *nss_sys::PK11_GetKeyData(sym_key.as_mut_ptr()) };
if u32::try_from(len)? > key_data.len {
return Err(ErrorKind::InternalError.into());
}

// # Safety
//
// This doesn't leak, because the SECItem* returned by PK11_GetKeyData
// just refers to a buffer managed by `sym_key` which we copy into `out`.
let buf = unsafe { sec_item_as_slice(&mut key_data)? };

Ok(buf.to_vec())
}

Expand Down Expand Up @@ -90,3 +98,189 @@ pub(crate) fn import_sym_key(
))
}
}

/// Check weather a primary password has been set and NSS needs to be authenticated.
/// Only available with the `keydb` feature.
#[cfg(feature = "keydb")]
pub fn authentication_with_primary_password_is_needed() -> Result<bool> {
let slot = slot::get_internal_key_slot()?;

unsafe {
Ok(
nss_sys::PK11_NeedLogin(slot.as_mut_ptr()) == nss_sys::PR_TRUE
&& nss_sys::PK11_IsLoggedIn(slot.as_mut_ptr(), ptr::null_mut()) != nss_sys::PR_TRUE,
)
}
}

/// Authorize NSS key store against a user-provided primary password.
/// Only available with the `keydb` feature.
#[cfg(feature = "keydb")]
pub fn authenticate_with_primary_password(primary_password: &str) -> Result<bool> {
let slot = slot::get_internal_key_slot()?;

let password_cstr = CString::new(primary_password).map_err(|_| ErrorKind::NulError)?;
unsafe {
Ok(
nss_sys::PK11_CheckUserPassword(slot.as_mut_ptr(), password_cstr.as_ptr())
== nss_sys::SECStatus::SECSuccess,
)
}
}

/// Retrieve a key, identified by `name`, from the internal NSS key store. If none exists, create
/// one, persist, and return.
/// Only available with the `keydb` feature.
#[cfg(feature = "keydb")]
pub fn get_or_create_aes256_key(name: &str) -> Result<Vec<u8>> {
let sym_key = match get_aes256_key(name) {
Ok(sym_key) => sym_key,
Err(_) => create_aes256_key(name)?,
};

let mut key_data = unsafe { *nss_sys::PK11_GetKeyData(sym_key.as_mut_ptr()) };
if key_data.len != nss_sys::AES_256_KEY_LENGTH {
return Err(ErrorKind::InvalidKeyLength.into());
}

// # Safety
//
// This doesn't leak, because the SECItem* returned by PK11_GetKeyData
// just refers to a buffer managed by `sym_key` which we copy into `out`.
let buf = unsafe { sec_item_as_slice(&mut key_data)? };

Ok(buf.to_vec())
}

#[cfg(feature = "keydb")]
fn get_aes256_key(name: &str) -> Result<SymKey> {
let slot = slot::get_internal_key_slot()?;
let name = CString::new(name).map_err(|_| ErrorKind::NulError)?;
let sym_key = unsafe {
SymKey::from_ptr(nss_sys::PK11_ListFixedKeysInSlot(
slot.as_mut_ptr(),
name.as_ptr() as *mut c_char,
ptr::null_mut(),
))
};
match sym_key {
Ok(sym_key) => {
// See
// https://searchfox.org/mozilla-central/source/security/manager/ssl/NSSKeyStore.cpp#163-201
// Unfortunately we can't use PK11_ExtractKeyValue(symKey.get()) here because softoken
// marks all token objects of type CKO_SECRET_KEY as sensitive. So we have to wrap and
// unwrap symKey to obtain a non-sensitive copy of symKey as a session object.
let wrapping_key = unsafe {
SymKey::from_ptr(nss_sys::PK11_KeyGen(
slot.as_mut_ptr(),
nss_sys::CKM_AES_KEY_GEN,
ptr::null_mut(),
16,
ptr::null_mut(),
))
.map_err(|_| get_last_error())?
};
let mut wrap_len = nss_sys::SECItem {
type_: nss_sys::SECItemType::siBuffer as u32,
data: ptr::null_mut(),
len: 0,
};
map_nss_secstatus(|| unsafe {
nss_sys::PK11_WrapSymKey(
nss_sys::CKM_AES_KEY_WRAP_KWP,
ptr::null_mut(),
wrapping_key.as_mut_ptr(),
sym_key.as_mut_ptr(),
&mut wrap_len,
)
})
.map_err(|_| get_last_error())?;
// PK11_UnwrapSymKey takes an int keySize
if wrap_len.len > u32::MAX - 8 {
return Err(ErrorKind::InvalidKeyLength.into());
}
// Allocate an extra 8 bytes for CKM_AES_KEY_WRAP_KWP overhead.
let wrapped_key = unsafe {
nss_sys::SECITEM_AllocItem(ptr::null_mut(), ptr::null_mut(), wrap_len.len + 8)
};
map_nss_secstatus(|| unsafe {
nss_sys::PK11_WrapSymKey(
nss_sys::CKM_AES_KEY_WRAP_KWP,
ptr::null_mut(),
wrapping_key.as_mut_ptr(),
sym_key.as_mut_ptr(),
wrapped_key,
)
})
.map_err(|_| get_last_error())?;
let sym_key = unsafe {
SymKey::from_ptr(nss_sys::PK11_UnwrapSymKey(
wrapping_key.as_mut_ptr(),
nss_sys::CKM_AES_KEY_WRAP_KWP,
ptr::null_mut(),
wrapped_key,
nss_sys::CKM_AES_GCM.into(),
(nss_sys::CKA_ENCRYPT | nss_sys::CKA_DECRYPT).into(),
wrap_len.len as i32,
))
}
.map_err(|_| get_last_error())?;

map_nss_secstatus(|| unsafe { nss_sys::PK11_ExtractKeyValue(sym_key.as_mut_ptr()) })?;
Ok(sym_key)
}
Err(e) => Err(e),
}
}

#[cfg(feature = "keydb")]
fn create_aes256_key(name: &str) -> Result<SymKey> {
let mut key_bytes: [u8; nss_sys::AES_256_KEY_LENGTH as usize] =
[0; nss_sys::AES_256_KEY_LENGTH as usize];
map_nss_secstatus(|| unsafe {
nss_sys::PK11_GenerateRandom(key_bytes.as_mut_ptr(), nss_sys::AES_256_KEY_LENGTH as i32)
})?;
match import_and_persist_sym_key(
nss_sys::CKM_AES_GCM.into(),
nss_sys::PK11Origin::PK11_OriginGenerated,
(nss_sys::CKA_ENCRYPT | nss_sys::CKA_DECRYPT).into(),
&key_bytes,
) {
Ok(sym_key) => {
let name = CString::new(name).map_err(|_| ErrorKind::NulError)?;
unsafe { nss_sys::PK11_SetSymKeyNickname(sym_key.as_mut_ptr(), name.as_ptr()) };
Ok(sym_key)
}
Err(e) => Err(e),
}
}

/// Safe wrapper around PK11_ImportSymKey that
/// de-allocates memory when the key goes out of
/// scope, and persists key in key4.db.
#[cfg(feature = "keydb")]
fn import_and_persist_sym_key(
mechanism: nss_sys::CK_MECHANISM_TYPE,
origin: nss_sys::PK11Origin,
operation: nss_sys::CK_ATTRIBUTE_TYPE,
buf: &[u8],
) -> Result<SymKey> {
let mut item = nss_sys::SECItem {
type_: nss_sys::SECItemType::siBuffer as u32,
data: buf.as_ptr() as *mut c_uchar,
len: c_uint::try_from(buf.len())?,
};
let slot = slot::get_internal_key_slot()?;
unsafe {
SymKey::from_ptr(nss_sys::PK11_ImportSymKeyWithFlags(
slot.as_mut_ptr(),
mechanism,
origin as u32,
operation,
&mut item,
nss_sys::CK_FLAGS::default(),
nss_sys::PR_TRUE,
ptr::null_mut(),
))
}
}
Loading

0 comments on commit aac3e37

Please sign in to comment.