Skip to content

Commit cea26ee

Browse files
committed
Fix TOCTOU race in get_or_create_master_key with double-checked locking
Two threads could both observe no master key and generate different keys, with the second writer silently overwriting the first. Use an ArcSwapOption cache for lock-free reads and a Mutex for serializing initialization.
1 parent 7533b24 commit cea26ee

File tree

3 files changed

+67
-18
lines changed

3 files changed

+67
-18
lines changed

rust/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/crates/cove-cspp/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ edition = "2024"
66
[dependencies]
77
cove-util = { path = "../cove-util" }
88

9+
arc-swap = { workspace = true }
910
hkdf = { workspace = true }
1011
sha2 = { workspace = true }
1112
rand = { workspace = true }

rust/crates/cove-cspp/src/cspp.rs

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::sync::{Arc, LazyLock, Mutex};
2+
3+
use arc_swap::ArcSwapOption;
14
use cove_util::encryption::Cryptor;
25

36
use crate::error::CsppError;
@@ -7,20 +10,64 @@ use crate::store::CsppStore;
710
const MASTER_KEY_NAME: &str = "cspp::v1::master_key";
811
const MASTER_KEY_ENCRYPTION_KEY_AND_NONCE: &str = "cspp::v1::master_key_encryption_key_and_nonce";
912

13+
static INIT_LOCK: Mutex<()> = Mutex::new(());
14+
static MASTER_KEY_CACHE: LazyLock<ArcSwapOption<[u8; 32]>> =
15+
LazyLock::new(|| ArcSwapOption::new(None));
16+
1017
pub struct Cspp<S: CsppStore>(S);
1118

1219
impl<S: CsppStore> Cspp<S> {
1320
pub fn new(store: S) -> Self {
1421
Self(store)
1522
}
1623

24+
/// Loads the master key from the store, or generates and saves a new one
25+
///
26+
/// Uses double-checked locking to prevent a TOCTOU race where two threads
27+
/// could both observe no key and generate different master keys
28+
pub fn get_or_create_master_key(&self) -> Result<MasterKey, CsppError> {
29+
// fast path (lock-free): return cached key
30+
if let Some(bytes) = MASTER_KEY_CACHE.load().as_deref() {
31+
return Ok(MasterKey::from_bytes(*bytes));
32+
}
33+
34+
// slow path: acquire init lock for double-checked initialization
35+
let _guard = INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
36+
37+
// re-check cache after acquiring lock
38+
if let Some(bytes) = MASTER_KEY_CACHE.load().as_deref() {
39+
return Ok(MasterKey::from_bytes(*bytes));
40+
}
41+
42+
// try loading from store
43+
if let Some(key) = self.get_master_key()? {
44+
MASTER_KEY_CACHE.store(Some(Arc::new(*key.as_bytes())));
45+
return Ok(key);
46+
}
47+
48+
// generate and save new key
49+
let key = MasterKey::generate();
50+
self.save_master_key(&key)?;
51+
MASTER_KEY_CACHE.store(Some(Arc::new(*key.as_bytes())));
52+
53+
Ok(key)
54+
}
55+
56+
/// Deletes the master key from the store
57+
pub fn delete_master_key(&self) -> bool {
58+
let _guard = INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
59+
MASTER_KEY_CACHE.store(None);
60+
self.0.delete(MASTER_KEY_NAME.into());
61+
self.0.delete(MASTER_KEY_ENCRYPTION_KEY_AND_NONCE.into())
62+
}
63+
1764
/// Saves the master key encrypted via the underlying store
1865
///
1966
/// The master key is encrypted with a random [`Cryptor`] before storage. The
2067
/// keychain already provides at-rest encryption, but this extra layer prevents
2168
/// the plaintext key from being accidentally exposed if other code enumerates
2269
/// keychain entries — it must be explicitly decrypted to be read
23-
pub fn save_master_key(&self, master_key: &MasterKey) -> Result<(), CsppError> {
70+
fn save_master_key(&self, master_key: &MasterKey) -> Result<(), CsppError> {
2471
let hex = hex::encode(master_key.as_bytes());
2572
let cryptor = Cryptor::new();
2673

@@ -40,8 +87,8 @@ impl<S: CsppStore> Cspp<S> {
4087
Ok(())
4188
}
4289

43-
/// Loads the master key from the store, returns None if not found
44-
pub fn get_master_key(&self) -> Result<Option<MasterKey>, CsppError> {
90+
/// Loads the master key, returns None if not found
91+
fn get_master_key(&self) -> Result<Option<MasterKey>, CsppError> {
4592
let Some(encryption_secret) = self.0.get(MASTER_KEY_ENCRYPTION_KEY_AND_NONCE.into()) else {
4693
return Ok(None);
4794
};
@@ -65,21 +112,9 @@ impl<S: CsppStore> Cspp<S> {
65112
Ok(Some(MasterKey::from_bytes(bytes)))
66113
}
67114

68-
/// Deletes the master key from the store
69-
pub fn delete_master_key(&self) -> bool {
70-
self.0.delete(MASTER_KEY_NAME.into());
71-
self.0.delete(MASTER_KEY_ENCRYPTION_KEY_AND_NONCE.into())
72-
}
73-
74-
/// Loads the master key from the store, or generates and saves a new one
75-
pub fn get_or_create_master_key(&self) -> Result<MasterKey, CsppError> {
76-
if let Some(key) = self.get_master_key()? {
77-
return Ok(key);
78-
}
79-
80-
let key = MasterKey::generate();
81-
self.save_master_key(&key)?;
82-
Ok(key)
115+
#[cfg(test)]
116+
fn reset_cache() {
117+
MASTER_KEY_CACHE.store(None);
83118
}
84119
}
85120

@@ -90,6 +125,9 @@ mod tests {
90125

91126
use super::*;
92127

128+
// serializes tests that touch the global MASTER_KEY_CACHE
129+
static CACHE_TEST_LOCK: Mutex<()> = Mutex::new(());
130+
93131
#[derive(Debug)]
94132
struct MockStore(Mutex<HashMap<String, String>>);
95133

@@ -122,6 +160,9 @@ mod tests {
122160

123161
#[test]
124162
fn master_key_store_and_load_roundtrip() {
163+
let _guard = CACHE_TEST_LOCK.lock().unwrap();
164+
Cspp::<MockStore>::reset_cache();
165+
125166
let cspp = mock_cspp();
126167
let original = MasterKey::generate();
127168
let original_bytes = *original.as_bytes();
@@ -141,6 +182,9 @@ mod tests {
141182

142183
#[test]
143184
fn get_or_create_creates_when_missing() {
185+
let _guard = CACHE_TEST_LOCK.lock().unwrap();
186+
Cspp::<MockStore>::reset_cache();
187+
144188
let cspp = mock_cspp();
145189
let first = cspp.get_or_create_master_key().unwrap();
146190
let first_bytes = *first.as_bytes();
@@ -151,6 +195,9 @@ mod tests {
151195

152196
#[test]
153197
fn get_or_create_reuses_existing() {
198+
let _guard = CACHE_TEST_LOCK.lock().unwrap();
199+
Cspp::<MockStore>::reset_cache();
200+
154201
let cspp = mock_cspp();
155202
let original = MasterKey::generate();
156203
let original_bytes = *original.as_bytes();

0 commit comments

Comments
 (0)