Skip to main content

dpp_crypto/keystore/
migration.rs

1use aes_gcm::{
2    Aes256Gcm, Nonce,
3    aead::{Aead, KeyInit, consts::U12},
4};
5use anyhow::{Context, Result};
6use rand::Rng;
7use zeroize::Zeroize;
8
9use super::crypto::derive_aes_key_argon2;
10use super::store::{KeyRecord, KeyRecordMap, KeyStore};
11
12impl KeyStore {
13    /// Open the key store, run `migrate_if_needed` if the store uses the legacy
14    /// SHA-256 KDF, and — if migration actually ran — re-open the file so
15    /// `self.cipher` reflects the new Argon2id key.
16    ///
17    /// This is the recommended entry point for production code. For stores
18    /// already at V2/V3 (Argon2id) it is identical to a single `open` call.
19    pub fn open_and_migrate(path: impl AsRef<std::path::Path>, passphrase: &str) -> Result<Self> {
20        let store = Self::open(path.as_ref(), passphrase)?;
21        if store.migrate_if_needed(passphrase)? {
22            // Re-open with the migrated file so the in-memory cipher is updated.
23            Self::open(path, passphrase)
24        } else {
25            Ok(store)
26        }
27    }
28
29    /// If this store was opened from a legacy format, re-encrypt all keys
30    /// with the Argon2id-derived key and persist. Call this once after
31    /// opening and verifying the passphrase works (e.g. by loading a key).
32    ///
33    /// Returns `true` if migration ran, `false` if the store was already at
34    /// V2/V3. Use `open_and_migrate` in production to avoid the post-migration
35    /// cipher inconsistency (this object's `self.cipher` is not updated here).
36    pub fn migrate_if_needed(&self, passphrase: &str) -> Result<bool> {
37        let needs = *self.needs_migration.read().expect("lock");
38        if !needs {
39            return Ok(false);
40        }
41
42        tracing::info!("migrating key store from SHA-256 to Argon2id KDF");
43
44        // Decrypt all records with the old cipher, re-encrypt with the new one.
45        let new_key = derive_aes_key_argon2(passphrase, &self.salt)?;
46        let new_cipher = Aes256Gcm::new(&new_key);
47
48        let mut map = self.records.write().expect("key store write lock");
49        let mut migrated = KeyRecordMap::with_capacity(map.len());
50
51        for (id, record) in map.iter() {
52            // Decrypt with legacy cipher.
53            let nonce = <&Nonce<U12>>::try_from(record.nonce.as_slice()).map_err(|_| {
54                anyhow::anyhow!(
55                    "stored nonce is not 12 bytes ({} bytes) for key {id} — corrupt or legacy record",
56                    record.nonce.len()
57                )
58            })?;
59            let mut raw = self
60                .cipher
61                .decrypt(nonce, record.encrypted_signing_key.as_ref())
62                .map_err(|_| {
63                    anyhow::anyhow!("AES-GCM decrypt failed during migration for key {id}")
64                })?;
65
66            // Re-encrypt with new cipher + fresh nonce.
67            let mut nonce_bytes = [0u8; 12];
68            crate::os_rng().fill_bytes(&mut nonce_bytes);
69            let new_nonce = <&Nonce<U12>>::from(&nonce_bytes);
70            let encrypted = new_cipher
71                .encrypt(new_nonce, raw.as_ref())
72                .map_err(|_| anyhow::anyhow!("AES-GCM encrypt failed during migration"))?;
73            raw.zeroize();
74
75            migrated.insert(
76                id.clone(),
77                KeyRecord {
78                    encrypted_signing_key: encrypted,
79                    nonce: nonce_bytes.to_vec(),
80                    ..record.clone()
81                },
82            );
83        }
84
85        *map = migrated;
86        self.persist_envelope(&map)
87            .context("Failed to persist migrated key store")?;
88
89        drop(map);
90        *self.needs_migration.write().expect("lock") = false;
91
92        // self.cipher still holds the old key; callers must use open_and_migrate
93        // (which re-opens the file) rather than continuing to use this object.
94        tracing::info!("key store migrated from SHA-256 to Argon2id");
95        Ok(true)
96    }
97}