Skip to main content

ps_datachunk/utils/
decrypt.rs

1use bytes::Bytes;
2use ps_cypher::DecryptionError;
3use ps_hash::Hash;
4
5use crate::{DataChunkError, OwnedDataChunk, Result};
6
7pub fn decrypt(encrypted: impl AsRef<[u8]>, key: &Hash) -> Result<OwnedDataChunk> {
8    let data = match ps_cypher::decrypt(encrypted.as_ref(), key) {
9        Ok(buffer) => Bytes::from_owner(buffer),
10        // match to ensure the key mismatch being removed fails compilation
11        Err(DecryptionError::KeyMismatch) => Err(DataChunkError::HashMismatch)?,
12        // misc failures are uninteresting and thus passed through
13        Err(err) => Err(err)?,
14    };
15
16    // we're relying on ps-cypher's integrity check
17    let chunk = OwnedDataChunk::from_parts_unchecked(data, *key);
18
19    Ok(chunk)
20}
21
22#[cfg(test)]
23#[allow(clippy::expect_used)]
24mod tests {
25    use chacha20poly1305::aead::{Aead, KeyInit};
26    use chacha20poly1305::ChaCha20Poly1305;
27
28    use crate::DataChunkError;
29
30    /// Number of ECC parity bytes per codeword, matching `ps_cypher::encrypt`.
31    const PARITY: u8 = 12;
32
33    /// A ciphertext sealed under key `K` by an attacker who knows `K` must be
34    /// rejected: its plaintext does not hash to `K`, so accepting it would
35    /// yield a chunk whose content does not match its address.
36    #[test]
37    fn decrypt_rejects_forged_ciphertext() {
38        let legitimate = b"legitimate chunk data";
39        let forged_plaintext = b"forged malicious data";
40
41        let key = ps_hash::hash(legitimate).expect("hashing should succeed");
42
43        // Seal the foreign plaintext with the key and nonce derived from `key`,
44        // mirroring ps-cypher's encryption pipeline.
45        let compressed =
46            ps_compress::compress(forged_plaintext).expect("compression should succeed");
47
48        let [_, _, nonce @ ..] = *key.parity();
49        let encryption_key = *key.digest();
50
51        let chacha = ChaCha20Poly1305::new(&encryption_key.into());
52        let sealed = chacha
53            .encrypt(&nonce.into(), compressed.as_ref())
54            .expect("sealing should succeed");
55        let forged = ps_ecc::encode(&sealed, PARITY).expect("ECC encoding should succeed");
56
57        let result = super::decrypt(&forged, &key);
58
59        assert!(matches!(result, Err(DataChunkError::HashMismatch)));
60    }
61}