multi-key 1.0.6

Multikey self-describing cryptographic key data
// SPDX-License-Identifier: Apache-2.0
use crate::{
    error::{AttributesError, CipherError, KdfError},
    AttrId, AttrView, CipherAttrView, CipherView, DataView, Error, FingerprintView, KdfAttrView,
    Multikey, Views,
};
#[cfg(feature = "legacy_chacha20_fallback")]
use chacha20::cipher::{KeyIvInit, StreamCipher};
#[cfg(feature = "legacy_chacha20_fallback")]
use chacha20::ChaCha20;
use chacha20::Nonce;
use multi_codec::Codec;
use multi_hash::{mh, Multihash};
use multi_trait::TryDecodeFrom;
use multi_util::Varuint;
use zeroize::Zeroizing;

use super::bcrypt::SALT_LENGTH;

pub const KEY_SIZE: usize = poly1305::KEY_SIZE;

/// Return the length of the [Nonce]
#[cfg(test)]
pub(crate) fn nonce_length() -> usize {
    Nonce::default().len()
}

pub(crate) struct View<'a> {
    mk: &'a Multikey,
    cipher: Option<&'a Multikey>,
}

impl<'a> View<'a> {
    pub fn new(mk: &'a Multikey, cipher: &'a Multikey) -> Self {
        Self {
            mk,
            cipher: Some(cipher),
        }
    }
}

impl<'a> TryFrom<&'a Multikey> for View<'a> {
    type Error = Error;

    fn try_from(mk: &'a Multikey) -> Result<Self, Self::Error> {
        Ok(Self { mk, cipher: None })
    }
}

impl<'a> AttrView for View<'a> {
    fn is_encrypted(&self) -> bool {
        if let Some(v) = self.mk.attributes.get(&AttrId::KeyIsEncrypted) {
            if let Ok((b, _)) = Varuint::<bool>::try_decode_from(v.as_slice()) {
                return b.to_inner();
            }
        }
        false
    }

    fn is_secret_key(&self) -> bool {
        true
    }

    fn is_public_key(&self) -> bool {
        false
    }

    fn is_secret_key_share(&self) -> bool {
        false
    }
}

impl<'a> DataView for View<'a> {
    /// return the ChaCha20 key bytes
    fn key_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
        let key = self
            .mk
            .attributes
            .get(&AttrId::KeyData)
            .ok_or(AttributesError::MissingKey)?;
        Ok(key.clone())
    }

    /// Check to see if this is a secret key before returning the key bytes
    fn secret_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
        if self.is_encrypted() {
            return Err(AttributesError::EncryptedKey.into());
        }
        self.key_bytes()
    }
}

impl<'a> CipherAttrView for View<'a> {
    fn cipher_codec(&self) -> Result<Codec, Error> {
        Ok(Codec::Chacha20Poly1305)
    }

    #[allow(deprecated)]
    fn nonce_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
        // try to look up the salt in the multikey attributes
        let nonce = self
            .mk
            .attributes
            .get(&AttrId::CipherNonce)
            .ok_or(CipherError::MissingNonce)?;

        let nonce = Nonce::clone_from_slice(nonce);

        Ok(nonce.to_vec().into())
    }

    fn key_length(&self) -> Result<usize, Error> {
        Ok(KEY_SIZE)
    }
}

impl<'a> KdfAttrView for View<'a> {
    fn kdf_codec(&self) -> Result<Codec, Error> {
        // try to look up the kdf codec in the multikey attributes
        let codec = self
            .mk
            .attributes
            .get(&AttrId::KdfCodec)
            .ok_or(KdfError::MissingCodec)?;
        Ok(Codec::try_from(codec.as_slice())?)
    }

    fn salt_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
        // try to look up the salt in the multikey attributes
        let salt = self
            .mk
            .attributes
            .get(&AttrId::KdfSalt)
            .ok_or(KdfError::MissingSalt)?;
        if salt.len() != SALT_LENGTH {
            Err(KdfError::InvalidSaltLen.into())
        } else {
            Ok(salt.clone())
        }
    }

    fn rounds(&self) -> Result<usize, Error> {
        // try to look up the rounds in the multikey attributes
        let rounds = self
            .mk
            .attributes
            .get(&AttrId::KdfRounds)
            .ok_or(KdfError::MissingRounds)?;
        Ok(Varuint::<usize>::try_from(rounds.as_slice())?.to_inner())
    }
}

impl<'a> CipherView for View<'a> {
    #[allow(deprecated)]
    fn decrypt(&self) -> Result<Multikey, Error> {
        let cipher = self.cipher.ok_or(CipherError::MissingCodec)?;
        // make sure the viewed key is an encrypted secret key
        let attr = self.mk.attr_view()?;
        if !attr.is_encrypted() || !attr.is_secret_key() {
            return Err(CipherError::DecryptionFailed.into());
        }

        // get the nonce data from the passed-in Multikey
        let nonce = {
            let cattr = cipher.cipher_attr_view()?;
            cattr.nonce_bytes()?
        };

        // create the chacha nonce from the data (only used by the legacy fallback)
        #[cfg(feature = "legacy_chacha20_fallback")]
        let n = Nonce::clone_from_slice(&nonce);

        // get the key data from the passed-in Multikey
        let key = {
            let kd = cipher.data_view()?;
            let key = kd.secret_bytes()?;
            if key.len() != self.key_length()? {
                return Err(CipherError::InvalidKey.into());
            }
            key
        };

        // create the chacha key from the data (only used by the legacy fallback)
        #[cfg(feature = "legacy_chacha20_fallback")]
        let k = chacha20::Key::clone_from_slice(&key);

        // get the encrypted key bytes from the viewed Multikey (self)
        let msg = {
            let attr = self.mk.data_view()?;
            attr.key_bytes()?
        };

        // Authenticated decryption (ChaCha20Poly1305): the stored ciphertext is
        // `plaintext || 16-byte Poly1305 tag`, so tampering or a wrong passphrase
        // is rejected rather than silently yielding garbage key bytes. This is
        // the only decryption path by default; the legacy bare-ChaCha20 fallback
        // for pre-AEAD keystores is gated behind the `legacy_chacha20_fallback`
        // feature flag (default off) so unauthenticated ciphertext can never be
        // returned as if it were valid.
        let dec = {
            use chacha20poly1305::aead::{Aead, KeyInit};
            use chacha20poly1305::{ChaCha20Poly1305, Nonce as AeadNonce};
            match ChaCha20Poly1305::new_from_slice(&key) {
                Ok(aead) => match aead.decrypt(AeadNonce::from_slice(&nonce), msg.as_slice()) {
                    Ok(pt) => Zeroizing::new(pt),
                    Err(_) => {
                        #[cfg(feature = "legacy_chacha20_fallback")]
                        {
                            // Legacy fallback: keys encrypted before
                            // authentication was added used bare ChaCha20 (no
                            // tag). Retry the legacy path so existing keystores
                            // keep opening; re-encrypting a key upgrades it to
                            // the authenticated format.
                            eprintln!(
                                "multi-key: AEAD decryption failed, falling back to \
                                 legacy unauthenticated ChaCha20 — re-encrypt this \
                                 key to upgrade to authenticated ChaCha20Poly1305"
                            );
                            let mut chacha = ChaCha20::new(&k, &n);
                            let mut legacy = msg.clone();
                            chacha.apply_keystream(&mut legacy);
                            legacy
                        }
                        #[cfg(not(feature = "legacy_chacha20_fallback"))]
                        {
                            return Err(CipherError::DecryptionFailed.into());
                        }
                    }
                },
                Err(_) => return Err(CipherError::InvalidKey.into()),
            }
        };

        // create a new Multikey from the viewed Multikey (self) with the
        // decrypted key and none of the kdf or cipher attributes
        let mut res = self.mk.clone();
        let _ = res.attributes.remove(&AttrId::KeyIsEncrypted);
        res.attributes.insert(AttrId::KeyData, dec);
        let _ = res.attributes.remove(&AttrId::CipherCodec);
        let _ = res.attributes.remove(&AttrId::CipherKeyLen);
        let _ = res.attributes.remove(&AttrId::CipherNonce);
        let _ = res.attributes.remove(&AttrId::KdfCodec);
        let _ = res.attributes.remove(&AttrId::KdfSalt);
        let _ = res.attributes.remove(&AttrId::KdfRounds);
        Ok(res)
    }

    #[allow(deprecated)]
    fn encrypt(&self) -> Result<Multikey, Error> {
        let cipher = self.cipher.ok_or(CipherError::MissingCodec)?;
        // make sure the viewed key is not encrypted
        let attr = self.mk.attr_view()?;
        if attr.is_encrypted() {
            return Err(
                CipherError::EncryptionFailed("key is encrypted already".to_string()).into(),
            );
        }

        // get the nonce data from the passed-in Multikey
        let nonce = {
            let cattr = cipher.cipher_attr_view()?;
            cattr.nonce_bytes()?
        };

        // get the key data from the passed-in Multikey
        let key = {
            let kd = cipher.data_view()?;
            let key = kd.secret_bytes()?;
            if key.len() != self.key_length()? {
                return Err(CipherError::InvalidKey.into());
            }
            key
        };

        // get the secret bytes from the viewed Multikey
        let msg = {
            let kd = self.mk.data_view()?;
            kd.secret_bytes()?
        };

        // Authenticated encryption (ChaCha20Poly1305): output is
        // `ciphertext || 16-byte Poly1305 tag` so integrity is verified on
        // decrypt. Same 32-byte key / 12-byte nonce as the bare cipher.
        let enc = {
            use chacha20poly1305::aead::{Aead, KeyInit};
            use chacha20poly1305::{ChaCha20Poly1305, Nonce as AeadNonce};
            let aead = ChaCha20Poly1305::new_from_slice(&key).map_err(|_| {
                CipherError::EncryptionFailed("invalid cipher key length".to_string())
            })?;
            let ct = aead
                .encrypt(AeadNonce::from_slice(&nonce), msg.as_slice())
                .map_err(|_| CipherError::EncryptionFailed("AEAD seal failed".to_string()))?;
            Zeroizing::new(ct)
        };

        // prepare the cipher attributes
        let cattr = cipher.cipher_attr_view()?;
        let cipher_codec: Vec<u8> = cipher.codec.into();
        let key_length: Vec<u8> = Varuint(cattr.key_length()?).into();
        let is_encrypted: Vec<u8> = Varuint(true).into();

        // get a view on the kdf attributes
        let kattr = cipher.kdf_attr_view()?;
        let kdf_codec: Vec<u8> = kattr.kdf_codec()?.into();
        let salt = kattr.salt_bytes()?;
        let rounds: Vec<u8> = Varuint(kattr.rounds()?).into();

        // create a copy of the viewed Multikey (self) and add in the encrypted
        // key data as awell as the kdf and cipher attributes and data so that
        // the encrypted Multikey is self-describing about how it was encrypted
        let mut res = self.mk.clone();
        res.attributes
            .insert(AttrId::KeyIsEncrypted, is_encrypted.into());
        res.attributes.insert(AttrId::KeyData, enc);
        res.attributes
            .insert(AttrId::CipherCodec, cipher_codec.into());
        res.attributes
            .insert(AttrId::CipherKeyLen, key_length.into());
        res.attributes.insert(AttrId::CipherNonce, nonce.clone());
        res.attributes.insert(AttrId::KdfCodec, kdf_codec.into());
        res.attributes.insert(AttrId::KdfSalt, salt.clone());
        res.attributes.insert(AttrId::KdfRounds, rounds.into());
        Ok(res)
    }
}

impl<'a> FingerprintView for View<'a> {
    fn fingerprint(&self, codec: Codec) -> Result<Multihash, Error> {
        // get the key bytes
        let bytes = {
            let kd = self.mk.data_view()?;
            kd.key_bytes()?
        };
        // hash the key bytes using the given codec
        Ok(mh::Builder::new_from_bytes(codec, bytes)?.try_build()?)
    }
}