Skip to main content

nodedb_wal/segment/
decrypt.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Segment-scoped payload decryption.
4//!
5//! Decryption belongs at the point records leave the WAL layer, not at each
6//! consumer. A record's AAD binds its ciphertext to the segment preamble it
7//! was written under, so only code that still holds the open segment knows the
8//! epoch needed to decrypt it. Pushing that knowledge outwards would mean every
9//! replay consumer re-deriving it — and any consumer that forgot would silently
10//! feed ciphertext into an engine decoder.
11//!
12//! [`SegmentDecryptor`] is therefore constructed once per segment — by the
13//! readers themselves, at open time — and applied to every record before it is
14//! handed out. A record that is marked encrypted with no key ring available is
15//! a hard [`WalError::EncryptedRecordWithoutKey`], never a passthrough and
16//! never a skip.
17
18use crate::crypto::KeyRing;
19use crate::error::{Result, WalError};
20use crate::preamble::{PREAMBLE_SIZE, SegmentPreamble};
21use crate::record::{RecordHeader, WalRecord};
22
23/// The per-segment inputs to the AAD: the epoch used to rebuild the nonce, and
24/// the preamble bytes that were prepended to the header at encryption time.
25struct SegmentAad {
26    epoch: [u8; 4],
27    preamble_bytes: [u8; PREAMBLE_SIZE],
28}
29
30/// Turns the records of one WAL segment back into plaintext.
31///
32/// Owns both the segment's AAD inputs and its key ring rather than borrowing
33/// them: a reader builds one at open time and keeps it for as long as the file
34/// handle lives, and borrowing the caller's ring would put a lifetime on every
35/// reader type that outlives the call that constructed it.
36pub struct SegmentDecryptor {
37    ring: Option<KeyRing>,
38    aad: Option<SegmentAad>,
39}
40
41impl SegmentDecryptor {
42    /// Build a decryptor for a segment from its preamble (absent on segments
43    /// written without encryption) and the replay key ring (absent when the
44    /// database is not configured for WAL encryption).
45    pub fn new(preamble: Option<&SegmentPreamble>, ring: Option<&KeyRing>) -> Self {
46        Self {
47            ring: ring.cloned(),
48            aad: preamble.map(|p| SegmentAad {
49                epoch: *p.epoch(),
50                preamble_bytes: p.to_bytes(),
51            }),
52        }
53    }
54
55    /// Return `record` as plaintext, with `ENCRYPTED_FLAG` cleared and its CRC
56    /// recomputed. Unencrypted records pass through untouched.
57    pub fn decrypt_record(&self, record: WalRecord) -> Result<WalRecord> {
58        if !record.is_encrypted() {
59            return Ok(record);
60        }
61        let (ring, aad) = self.require_keys(record.header.lsn)?;
62        record.into_decrypted(&aad.epoch, Some(&aad.preamble_bytes), Some(ring))
63    }
64
65    /// Return the plaintext for a payload that was read separately from its
66    /// header, as the lazy reader does. Unencrypted payloads pass through.
67    pub fn decrypt_payload(&self, header: &RecordHeader, payload: Vec<u8>) -> Result<Vec<u8>> {
68        let record = WalRecord {
69            header: *header,
70            payload,
71        };
72        if !record.is_encrypted() {
73            return Ok(record.payload);
74        }
75        let (ring, aad) = self.require_keys(header.lsn)?;
76        record.decrypt_payload_ring(&aad.epoch, Some(&aad.preamble_bytes), Some(ring))
77    }
78
79    /// Resolve the key ring and segment AAD, or explain which one is missing.
80    fn require_keys(&self, lsn: u64) -> Result<(&KeyRing, &SegmentAad)> {
81        const SITE: &str = "WAL segment replay";
82        let ring = match self.ring.as_ref() {
83            Some(ring) => ring,
84            None => {
85                let err = WalError::EncryptedRecordWithoutKey { lsn, context: SITE };
86                crate::diag::encrypted_record_without_key(&err, lsn, SITE);
87                return Err(err);
88            }
89        };
90        // An encrypted record can only exist in a segment whose preamble
91        // recorded the epoch it was encrypted under. A missing preamble means
92        // the segment's leading bytes are gone, not that the record is legible.
93        let aad = self.aad.as_ref().ok_or_else(|| WalError::CorruptRecord {
94            lsn,
95            detail: "encrypted record in a segment with no preamble — the epoch \
96                     needed to decrypt it is unrecoverable"
97                .into(),
98        })?;
99        Ok((ring, aad))
100    }
101}