Open source

Read the code that locks your vault

This is the exact encryption core that runs on your device: the same file, compiled to WebAssembly. Pick a file to see what it does and where it sits in the flow.

vault.rs — potent-cipher-core
vault.rs

crates › core › src › vault.rs

//! Vaults and items. Every item has its own random key, wrapped by its vault's key. Ciphertexts are bound to the
//! item id and vault id, so the server can't move or swap them.

use zeroize::Zeroize;

use crate::account::Keyring;
use crate::error::{Error, Result};
use crate::keys::{KdfParams, Key32};
use crate::primitives::{aad, argon2id, hkdf, open, random_bytes, seal, unwrap_key, wrap_key, SALT_LEN};

const AAD_VK: &[u8] = b"pc/v1/vk";
const AAD_VK_CUSTOM: &[u8] = b"pc/v1/vk-custom";
const AAD_IK: &[u8] = b"pc/v1/ik";
const AAD_ITEM: &[u8] = b"pc/v1/item";
const INFO_FILE: &[u8] = b"pc/v1/pcipher-vault";

/// An encrypted item as stored on the server.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SealedItem {
    /// The item key, wrapped by the vault key.
    pub wrapped_item_key: Vec<u8>,
    /// The item content (label, value, notes...), encrypted with the item key.
    pub ciphertext: Vec<u8>,
}

impl Keyring {
    /// Creates a vault protected by the master key. Returns the wrapped vault key to store.
    pub fn create_vault(&mut self, vault_id: &str) -> Vec<u8> {
        let vault_key = Key32::random();
        let wrapped = wrap_key(&self.master, &vault_key, &aad(&[AAD_VK, vault_id.as_bytes()]));
        self.vaults.insert(vault_id.to_owned(), vault_key);
        wrapped
    }

    /// Creates a vault locked by its own passphrase (Pro). Returns the salt and wrapped vault key to store.
    pub fn create_custom_vault(
        &mut self,
        vault_id: &str,
        vault_passphrase: &[u8],
        kdf: KdfParams,
    ) -> Result<([u8; SALT_LEN], Vec<u8>)> {
        let salt = random_bytes::<SALT_LEN>();
        let kek = argon2id(vault_passphrase, &salt, &kdf)?;
        let vault_key = Key32::random();
        let wrapped = wrap_key(&kek, &vault_key, &aad(&[AAD_VK_CUSTOM, vault_id.as_bytes()]));
        self.vaults.insert(vault_id.to_owned(), vault_key);
        Ok((salt, wrapped))
    }

    /// Opens a master-key vault so its items can be read and written.
    pub fn open_vault(&mut self, vault_id: &str, wrapped_vault_key: &[u8]) -> Result<()> {
        let vault_key = unwrap_key(&self.master, wrapped_vault_key, &aad(&[AAD_VK, vault_id.as_bytes()]))?;
        self.vaults.insert(vault_id.to_owned(), vault_key);
        Ok(())
    }

    /// Opens a custom-key vault with its own passphrase.
    pub fn open_custom_vault(
        &mut self,
        vault_id: &str,
        vault_passphrase: &[u8],
        salt: &[u8; SALT_LEN],
        kdf: KdfParams,
        wrapped_vault_key: &[u8],
    ) -> Result<()> {
        let kek = argon2id(vault_passphrase, salt, &kdf)?;
        let vault_key = unwrap_key(&kek, wrapped_vault_key, &aad(&[AAD_VK_CUSTOM, vault_id.as_bytes()]))?;
        self.vaults.insert(vault_id.to_owned(), vault_key);
        Ok(())
    }

    /// Forgets one vault's key (e.g. locking a custom-key vault again).
    pub fn close_vault(&mut self, vault_id: &str) {
        self.vaults.remove(vault_id);
    }

    /// Whether a vault is open in this keyring.
    pub fn is_vault_open(&self, vault_id: &str) -> bool {
        self.vaults.contains_key(vault_id)
    }

    fn vault_key(&self, vault_id: &str) -> Result<&Key32> {
        self.vaults.get(vault_id).ok_or(Error::VaultNotOpen)
    }

    /// Encrypts an item's content with a fresh item key.
    pub fn encrypt_item(&self, vault_id: &str, item_id: &str, plaintext: &[u8]) -> Result<SealedItem> {
        let vault_key = self.vault_key(vault_id)?;
        let item_key = Key32::random();
        let wrapped_item_key = wrap_key(vault_key, &item_key, &aad(&[AAD_IK, vault_id.as_bytes(), item_id.as_bytes()]));
        let ciphertext = seal(&item_key, plaintext, &aad(&[AAD_ITEM, vault_id.as_bytes(), item_id.as_bytes()]));
        Ok(SealedItem { wrapped_item_key, ciphertext })
    }

    /// Decrypts an item made by [`Keyring::encrypt_item`].
    pub fn decrypt_item(&self, vault_id: &str, item_id: &str, item: &SealedItem) -> Result<Vec<u8>> {
        let vault_key = self.vault_key(vault_id)?;
        let item_key = unwrap_key(
            vault_key,
            &item.wrapped_item_key,
            &aad(&[AAD_IK, vault_id.as_bytes(), item_id.as_bytes()]),
        )?;
        open(&item_key, &item.ciphertext, &aad(&[AAD_ITEM, vault_id.as_bytes(), item_id.as_bytes()]))
    }

    /// The key a `.pcipher` file in vault-key mode uses: derived from the vault key and the file's own salt.
    pub(crate) fn file_key(&self, vault_id: &str, file_salt: &[u8]) -> Result<Key32> {
        let vault_key = self.vault_key(vault_id)?;
        let mut info = INFO_FILE.to_vec();
        info.extend_from_slice(&aad(&[vault_id.as_bytes()]));
        let key = hkdf(vault_key.as_bytes(), file_salt, &info);
        info.zeroize();
        Ok(key)
    }
}

#[cfg(test)]
mod tests {
    use crate::account::create_account;
    use crate::error::Error;
    use crate::keys::KdfParams;

    fn fast_kdf() -> KdfParams {
        KdfParams { memory_kib: KdfParams::MIN_MEMORY_KIB, iterations: 2, parallelism: 1 }
    }

    #[test]
    fn items_round_trip_and_are_bound_to_their_ids() {
        let mut account = create_account(b"a passphrase for tests", fast_kdf()).unwrap();
        let wrapped = account.keyring.create_vault("vault-1");
        let item = account.keyring.encrypt_item("vault-1", "item-1", b"{\"label\":\"x\"}").unwrap();
        assert_eq!(account.keyring.decrypt_item("vault-1", "item-1", &item).unwrap(), b"{\"label\":\"x\"}");

        // A server that swaps the item id or vault id gets nothing.
        assert_eq!(account.keyring.decrypt_item("vault-1", "item-2", &item).err(), Some(Error::Decrypt));

        // The vault reopens from its wrapped key after locking.
        account.keyring.close_vault("vault-1");
        assert_eq!(account.keyring.decrypt_item("vault-1", "item-1", &item).err(), Some(Error::VaultNotOpen));
        account.keyring.open_vault("vault-1", &wrapped).unwrap();
        assert!(account.keyring.decrypt_item("vault-1", "item-1", &item).is_ok());
        assert!(account.keyring.open_vault("vault-2", &wrapped).is_err());
    }

    #[test]
    fn custom_vaults_need_their_own_passphrase() {
        let mut account = create_account(b"account passphrase 1", fast_kdf()).unwrap();
        let (salt, wrapped) = account.keyring.create_custom_vault("v", b"vault secret words", fast_kdf()).unwrap();
        account.keyring.close_vault("v");
        assert!(account.keyring.open_vault("v", &wrapped).is_err());
        assert!(account.keyring.open_custom_vault("v", b"wrong words here", &salt, fast_kdf(), &wrapped).is_err());
        account.keyring.open_custom_vault("v", b"vault secret words", &salt, fast_kdf(), &wrapped).unwrap();
        assert!(account.keyring.is_vault_open("v"));
    }
}
Last updated Nov 10, 2026, 3:36 PM UTCcommit f8df724156 linesrust

Try it with the real code

Type something and a passphrase. Your browser runs the core above and shows the .pcipher bytes it made. Nothing is sent anywhere.

Header (73 bytes, readable)

Magic · 7··
Version · 1··
Mode · 1··
KDF · 1··
Argon2id params · 12··
Salt · 16··
Key id · 8··
Chunk size · 4··
Metadata length · 4··
Nonce prefix · 19··

Encrypted body (looks random)

··