nss/pk11/
sym_key.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5#[cfg(feature = "keydb")]
6use crate::pk11::types::Slot;
7#[cfg(feature = "keydb")]
8use crate::util::get_last_error;
9use crate::{
10    error::*,
11    pk11::{context::HashAlgorithm, slot, types::SymKey},
12    util::{assert_nss_initialized, map_nss_secstatus, sec_item_as_slice, ScopedPtr},
13};
14#[cfg(feature = "keydb")]
15use std::ffi::{c_char, CString};
16#[cfg(feature = "keydb")]
17use std::sync::{Mutex, OnceLock};
18use std::{
19    mem,
20    os::raw::{c_uchar, c_uint, c_ulong},
21    ptr,
22};
23
24// Serialize global state dependent function calls
25#[cfg(feature = "keydb")]
26static GLOBAL_TOKEN_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
27
28pub fn hkdf_expand(
29    digest_alg: &HashAlgorithm,
30    key_bytes: &[u8],
31    info: &[u8],
32    len: usize,
33) -> Result<Vec<u8>> {
34    assert_nss_initialized();
35    let mech = digest_alg.as_hkdf_mechanism();
36    // Most of the following code is inspired by the Firefox WebCrypto implementation:
37    // https://searchfox.org/mozilla-central/rev/ee3905439acbf81e9c829ece0b46d09d2fa26c5c/dom/crypto/WebCryptoTask.cpp#2530-2597
38    // Except that we only do the expand part, which explains why we use null pointers below.
39    let mut hkdf_params = nss_sys::CK_NSS_HKDFParams {
40        bExtract: nss_sys::CK_FALSE,
41        pSalt: ptr::null_mut(),
42        ulSaltLen: 0,
43        bExpand: nss_sys::CK_TRUE,
44        pInfo: info.as_ptr() as *mut u8,
45        ulInfoLen: c_ulong::try_from(info.len())?,
46    };
47    let mut params = nss_sys::SECItem {
48        type_: nss_sys::SECItemType::siBuffer as u32,
49        data: &mut hkdf_params as *mut _ as *mut c_uchar,
50        len: u32::try_from(mem::size_of::<nss_sys::CK_NSS_HKDFParams>())?,
51    };
52    let base_key = import_sym_key(mech.into(), nss_sys::CKA_WRAP.into(), key_bytes)?;
53    let derived_len = i32::try_from(len)?;
54    let sym_key = unsafe {
55        SymKey::from_ptr(
56            // CKM_SHA512_HMAC and CKA_SIGN are key type and usage attributes of the
57            // derived symmetric key and don't matter because we ignore them anyway.
58            nss_sys::PK11_Derive(
59                base_key.as_mut_ptr(),
60                mech.into(),
61                &mut params,
62                nss_sys::CKM_SHA512_HMAC.into(),
63                nss_sys::CKA_SIGN.into(),
64                derived_len,
65            ),
66        )?
67    };
68    map_nss_secstatus(|| unsafe { nss_sys::PK11_ExtractKeyValue(sym_key.as_mut_ptr()) })?;
69    // SAFETY: This doesn't leak, because the SECItem* returned by PK11_GetKeyData just refers to a
70    // buffer managed by `sym_key` which we copy into `out`.
71    let mut key_data = unsafe { *nss_sys::PK11_GetKeyData(sym_key.as_mut_ptr()) };
72    if u32::try_from(len)? > key_data.len {
73        return Err(ErrorKind::InternalError.into());
74    }
75    let buf = unsafe { sec_item_as_slice(&mut key_data)? };
76    Ok(buf.to_vec())
77}
78
79/// Safe wrapper around PK11_ImportSymKey that
80/// de-allocates memory when the key goes out of
81/// scope.
82pub(crate) fn import_sym_key(
83    mechanism: nss_sys::CK_MECHANISM_TYPE,
84    operation: nss_sys::CK_ATTRIBUTE_TYPE,
85    buf: &[u8],
86) -> Result<SymKey> {
87    assert_nss_initialized();
88    let mut item = nss_sys::SECItem {
89        type_: nss_sys::SECItemType::siBuffer as u32,
90        data: buf.as_ptr() as *mut c_uchar,
91        len: c_uint::try_from(buf.len())?,
92    };
93    let slot = slot::get_internal_slot()?;
94    unsafe {
95        SymKey::from_ptr(nss_sys::PK11_ImportSymKey(
96            slot.as_mut_ptr(),
97            mechanism,
98            nss_sys::PK11Origin::PK11_OriginUnwrap as u32,
99            operation,
100            &mut item,
101            ptr::null_mut(),
102        ))
103    }
104}
105
106/// Check weather a primary password has been set and NSS needs to be authenticated.
107/// Only available with the `keydb` feature.
108#[cfg(feature = "keydb")]
109pub fn authentication_with_primary_password_is_needed() -> Result<bool> {
110    // PK11_IsLoggedIn depends on the token state
111    let lock = GLOBAL_TOKEN_LOCK.get_or_init(|| Mutex::new(()));
112    let _guard = lock.lock().unwrap();
113
114    let slot = slot::get_internal_key_slot()?;
115
116    unsafe {
117        Ok(
118            nss_sys::PK11_NeedLogin(slot.as_mut_ptr()) == nss_sys::PR_TRUE
119                && nss_sys::PK11_IsLoggedIn(slot.as_mut_ptr(), ptr::null_mut()) != nss_sys::PR_TRUE,
120        )
121    }
122}
123
124/// Authorize NSS key store against a user-provided primary password.
125/// Only available with the `keydb` feature.
126#[cfg(feature = "keydb")]
127pub fn authenticate_with_primary_password(primary_password: &str) -> Result<bool> {
128    // this needs serializing because PK11_CheckUserPassword first loggs the user out
129    let lock = GLOBAL_TOKEN_LOCK.get_or_init(|| Mutex::new(()));
130    let _guard = lock.lock().unwrap();
131
132    let slot = slot::get_internal_key_slot()?;
133
134    let password_cstr = CString::new(primary_password).map_err(|_| ErrorKind::NulError)?;
135    unsafe {
136        Ok(
137            nss_sys::PK11_CheckUserPassword(slot.as_mut_ptr(), password_cstr.as_ptr())
138                == nss_sys::SECStatus::SECSuccess,
139        )
140    }
141}
142
143/// Retrieve a key, identified by `name`, from the internal NSS key store. If none exists, create
144/// one, persist, and return.
145/// Only available with the `keydb` feature.
146#[cfg(feature = "keydb")]
147pub fn get_or_create_aes256_key(name: &str) -> Result<Vec<u8>> {
148    let sym_key = match get_aes256_key(name)? {
149        Some(sym_key) => sym_key,
150        None => create_aes256_key(name)?,
151    };
152    let mut key_data = unsafe { *nss_sys::PK11_GetKeyData(sym_key.as_mut_ptr()) };
153    if key_data.len != nss_sys::AES_256_KEY_LENGTH {
154        return Err(ErrorKind::InvalidKeyLength.into());
155    }
156    let buf = unsafe { sec_item_as_slice(&mut key_data)? };
157    // SAFETY: `to_vec` copies the data out before there's any chance for `sym_key` to be
158    // destroyed.
159    Ok(buf.to_vec())
160}
161
162#[cfg(feature = "keydb")]
163pub fn get_aes256_key(name: &str) -> Result<Option<SymKey>> {
164    // PK11_ListFixedKeysInSlot depends on the token to be unlocked
165    let lock = GLOBAL_TOKEN_LOCK.get_or_init(|| Mutex::new(()));
166    let _guard = lock.lock().unwrap();
167
168    let slot = slot::get_internal_key_slot()?;
169    let c_name = CString::new(name).map_err(|_| ErrorKind::NulError)?;
170    let sym_key = unsafe {
171        // PK11_ListFixedKeysInSlot returns either a key or null.
172        SymKey::from_ptr(nss_sys::PK11_ListFixedKeysInSlot(
173            slot.as_mut_ptr(),
174            c_name.as_ptr() as *mut c_char,
175            ptr::null_mut(),
176        ))
177    };
178    match sym_key {
179        Ok(sym_key) => match extract_aes256_key_value(slot, sym_key) {
180            Ok(key) => Ok(Some(key)),
181            Err(e) => Err(e),
182        },
183        Err(_) => Ok(None),
184    }
185}
186
187#[cfg(feature = "keydb")]
188// See
189// https://searchfox.org/mozilla-central/source/security/manager/ssl/NSSKeyStore.cpp#163-201
190//
191// Unfortunately we can't use PK11_ExtractKeyValue(symKey.get()) here because softoken
192// marks all token objects of type CKO_SECRET_KEY as sensitive. So we have to wrap and
193// unwrap symKey to obtain a non-sensitive copy of symKey as a session object.
194fn extract_aes256_key_value(slot: Slot, sym_key: SymKey) -> Result<SymKey> {
195    let wrapping_key = unsafe {
196        SymKey::from_ptr(nss_sys::PK11_KeyGen(
197            slot.as_mut_ptr(),
198            nss_sys::CKM_AES_KEY_GEN,
199            ptr::null_mut(),
200            16,
201            ptr::null_mut(),
202        ))
203        .map_err(|_| get_last_error())?
204    };
205
206    // Allocate an extra 8 bytes for CKM_AES_KEY_WRAP_KWP overhead.
207    let mut buf = vec![
208        0;
209        (nss_sys::AES_256_KEY_LENGTH + 8)
210            .try_into()
211            .expect("invalid key length")
212    ];
213    let mut wrapped_key = nss_sys::SECItem {
214        type_: nss_sys::SECItemType::siBuffer as u32,
215        data: buf.as_mut_ptr(),
216        len: buf.len() as u32,
217    };
218
219    // This operation can fail if the sym_key is corrupt.
220    // If thats the case, the underlying NSC_WrapKey would return a
221    // CKR_KEY_TYPE_INCONSISTENT error, which won't get propagated to us here,
222    // though.
223    map_nss_secstatus(|| unsafe {
224        nss_sys::PK11_WrapSymKey(
225            nss_sys::CKM_AES_KEY_WRAP_KWP,
226            ptr::null_mut(),
227            wrapping_key.as_mut_ptr(),
228            sym_key.as_mut_ptr(),
229            &mut wrapped_key,
230        )
231    })
232    .map_err(|_| get_last_error())?;
233
234    let sym_key = unsafe {
235        SymKey::from_ptr(nss_sys::PK11_UnwrapSymKey(
236            wrapping_key.as_mut_ptr(),
237            nss_sys::CKM_AES_KEY_WRAP_KWP,
238            ptr::null_mut(),
239            &mut wrapped_key,
240            nss_sys::CKM_AES_GCM.into(),
241            (nss_sys::CKA_ENCRYPT | nss_sys::CKA_DECRYPT).into(),
242            nss_sys::AES_256_KEY_LENGTH as i32,
243        ))
244    }
245    .map_err(|_| get_last_error())?;
246
247    map_nss_secstatus(|| unsafe { nss_sys::PK11_ExtractKeyValue(sym_key.as_mut_ptr()) })?;
248
249    Ok(sym_key)
250}
251
252#[cfg(feature = "keydb")]
253fn create_aes256_key(name: &str) -> Result<SymKey> {
254    let mut key_bytes: [u8; nss_sys::AES_256_KEY_LENGTH as usize] =
255        [0; nss_sys::AES_256_KEY_LENGTH as usize];
256    map_nss_secstatus(|| unsafe {
257        nss_sys::PK11_GenerateRandom(key_bytes.as_mut_ptr(), nss_sys::AES_256_KEY_LENGTH as i32)
258    })?;
259    match import_and_persist_sym_key(
260        nss_sys::CKM_AES_GCM.into(),
261        nss_sys::PK11Origin::PK11_OriginGenerated,
262        (nss_sys::CKA_ENCRYPT | nss_sys::CKA_DECRYPT).into(),
263        &key_bytes,
264    ) {
265        Ok(sym_key) => {
266            let name = CString::new(name).map_err(|_| ErrorKind::NulError)?;
267            unsafe { nss_sys::PK11_SetSymKeyNickname(sym_key.as_mut_ptr(), name.as_ptr()) };
268            Ok(sym_key)
269        }
270        Err(e) => Err(e),
271    }
272}
273
274/// Safe wrapper around PK11_ImportSymKey that
275/// de-allocates memory when the key goes out of
276/// scope, and persists key in key4.db.
277#[cfg(feature = "keydb")]
278fn import_and_persist_sym_key(
279    mechanism: nss_sys::CK_MECHANISM_TYPE,
280    origin: nss_sys::PK11Origin,
281    operation: nss_sys::CK_ATTRIBUTE_TYPE,
282    buf: &[u8],
283) -> Result<SymKey> {
284    // PK11_ImportSymKeyWithFlags depends on the token to be unlocked in order
285    // to encrypt the key
286    let lock = GLOBAL_TOKEN_LOCK.get_or_init(|| Mutex::new(()));
287    let _guard = lock.lock().unwrap();
288
289    let mut item = nss_sys::SECItem {
290        type_: nss_sys::SECItemType::siBuffer as u32,
291        data: buf.as_ptr() as *mut c_uchar,
292        len: c_uint::try_from(buf.len())?,
293    };
294    let slot = slot::get_internal_key_slot()?;
295    unsafe {
296        SymKey::from_ptr(nss_sys::PK11_ImportSymKeyWithFlags(
297            slot.as_mut_ptr(),
298            mechanism,
299            origin as u32,
300            operation,
301            &mut item,
302            nss_sys::CK_FLAGS::default(),
303            nss_sys::PR_TRUE,
304            ptr::null_mut(),
305        ))
306    }
307}
308
309#[cfg(feature = "keydb")]
310#[cfg(test)]
311mod keydb_test {
312    use super::*;
313    use crate::ensure_initialized_with_profile_dir;
314    use std::path::PathBuf;
315    use std::thread;
316
317    fn profile_path() -> PathBuf {
318        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/profile")
319    }
320
321    #[test]
322    fn test_get_aes256_key_not_found() {
323        ensure_initialized_with_profile_dir(profile_path());
324        authenticate_with_primary_password("password").unwrap();
325        let result = get_aes256_key("unknown-key").unwrap();
326        assert!(result.is_none());
327    }
328
329    #[test]
330    fn test_get_aes256_key() {
331        ensure_initialized_with_profile_dir(profile_path());
332        authenticate_with_primary_password("password").unwrap();
333        let result = get_aes256_key("as-logins-key").unwrap();
334        assert!(result.is_some());
335    }
336
337    #[test]
338    fn test_get_aes256_key_parallel() {
339        ensure_initialized_with_profile_dir(profile_path());
340
341        let threads: Vec<_> = (0..100)
342            .map(|_| {
343                thread::spawn(move || {
344                    let authenticated = authenticate_with_primary_password("password").unwrap();
345                    assert!(authenticated);
346                    let result = get_aes256_key("as-logins-key").unwrap();
347                    assert!(result.is_some());
348                })
349            })
350            .collect();
351
352        for handle in threads {
353            handle.join().unwrap();
354        }
355    }
356}