Skip to main content

dpp_crypto/keystore/
migration.rs

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