1.2.0: mux loss-concealment read path (P3/Edit-2, A2 NULL-TS fill)
Decrypt-verify is a RIP gate, not a MUX gate. On the mux read path an undecryptable content unit must never abort the mux: - DecryptingSectorSource gains tolerate_decrypt_loss(): when set, an undecryptable in-content unit is tallied, overwritten with valid NULL TS packets (PID 0x1FFF) via aacs::fill_null_ts_unit, logged loud with its LBA, and the read returns Ok — the stream keeps flowing. The rip paths keep the fail-loud DECRYPT_VERIFY_READ decorator (re-read off the disc); only the mux opts in. - Wire it into both mux read paths: the file-backed highway (build_iso_pipeline) and the inline DiscStream. - NULL-TS fill keeps the demuxer byte-synced on the 192-byte stride; the lost video/audio PID packets surface as a CC gap the TS assembler already drops a partial PES on (the B1 foundation). Ciphertext is never passed downstream either way. - Fix stale resolve_vid_only no-cert test: default is UHD (audit #4). Tests: conceal-as-NULL-TS, fill well-formedness, fail-loud still holds.
This commit is contained in:
@@ -163,6 +163,44 @@ pub fn aacs_unit_needs_decrypt(unit: &[u8]) -> bool {
|
|||||||
aacs_unit_encrypted(unit) && ts_sync_destroyed(unit)
|
aacs_unit_encrypted(unit) && ts_sync_destroyed(unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Overwrite an aligned unit (6144 bytes) IN PLACE with valid NULL MPEG-TS
|
||||||
|
/// source packets — the [A2] mux loss-concealment fill for a content unit that
|
||||||
|
/// genuinely would not decrypt.
|
||||||
|
///
|
||||||
|
/// Zero-filling such a unit is wrong at the TS layer: a run of `0x00` bytes
|
||||||
|
/// carries no `0x47` sync, so the demuxer loses packet framing and can mis-parse
|
||||||
|
/// the *next* unit if a stray `0x47` appears mid-zero. Instead we lay down 32
|
||||||
|
/// well-formed BD source packets, each a TS null packet (PID `0x1FFF`):
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// [4-byte TP_extra_header = 0][47 1F FF 10][184 bytes 0xFF stuffing]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The demuxer stays byte-synced on the 192-byte stride, and because PID
|
||||||
|
/// `0x1FFF` matches no elementary stream every null packet is silently dropped —
|
||||||
|
/// so the *video/audio* PID simply loses these packets. That shows up downstream
|
||||||
|
/// as a continuity-counter gap on the real PID, which the TS assembler already
|
||||||
|
/// turns into a dropped partial PES (see `mux::ts`), the foundation B1 builds on.
|
||||||
|
/// This NEVER emits ciphertext and is lossless framing, not fabricated content.
|
||||||
|
pub fn fill_null_ts_unit(unit: &mut [u8]) {
|
||||||
|
const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192
|
||||||
|
let mut off = 0;
|
||||||
|
while off + PKT <= unit.len() {
|
||||||
|
// TP_extra_header (arrival timestamp / copy-control) — zero is fine; the
|
||||||
|
// demuxer never reads it for a PID it does not track.
|
||||||
|
unit[off..off + 4].fill(0);
|
||||||
|
// 188-byte TS null packet: sync, PID 0x1FFF (no PUSI/TEI), payload-only
|
||||||
|
// with continuity counter 0.
|
||||||
|
unit[off + 4] = TS_SYNC; // 0x47
|
||||||
|
unit[off + 5] = 0x1F; // PID high (top 5 bits of 0x1FFF, flags clear)
|
||||||
|
unit[off + 6] = 0xFF; // PID low
|
||||||
|
unit[off + 7] = 0x10; // adaptation=01 (payload only), CC=0
|
||||||
|
// Stuffing: 0xFF is the conventional null-packet payload fill.
|
||||||
|
unit[off + 8..off + PKT].fill(0xFF);
|
||||||
|
off += PKT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Count the MPEG-TS sync bytes (`0x47`) present at the BD-TS packet stride
|
/// Count the MPEG-TS sync bytes (`0x47`) present at the BD-TS packet stride
|
||||||
/// (offset 4 and every 192 bytes after — 4-byte TP_extra_header + 188-byte
|
/// (offset 4 and every 192 bytes after — 4-byte TP_extra_header + 188-byte
|
||||||
/// TS packet). A clear or correctly-decrypted m2ts unit shows ~one per
|
/// TS packet). A clear or correctly-decrypted m2ts unit shows ~one per
|
||||||
|
|||||||
+2
-2
@@ -45,8 +45,8 @@ pub use trace::{KeyNode, KeyOutcome, KeyStep, ResolutionTrace, UnlockOutcome, Un
|
|||||||
pub use decrypt::{
|
pub use decrypt::{
|
||||||
ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, UnitKeyResult, aacs_unit_encrypted,
|
ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, UnitKeyResult, aacs_unit_encrypted,
|
||||||
aacs_unit_needs_decrypt, decrypt_bus, decrypt_unit, decrypt_unit_checked, decrypt_unit_full,
|
aacs_unit_needs_decrypt, decrypt_bus, decrypt_unit, decrypt_unit_checked, decrypt_unit_full,
|
||||||
decrypt_unit_try_keys, is_unit_aligned, ts_packet_total, ts_sync_count, ts_sync_destroyed,
|
decrypt_unit_try_keys, fill_null_ts_unit, is_unit_aligned, ts_packet_total, ts_sync_count,
|
||||||
unit_is_clean_ps, unit_is_clean_ts, unit_key_validates,
|
ts_sync_destroyed, unit_is_clean_ps, unit_is_clean_ts, unit_key_validates,
|
||||||
};
|
};
|
||||||
// `probe` is a reproduction-harness helper (see keys.rs), not part of the
|
// `probe` is a reproduction-harness helper (see keys.rs), not part of the
|
||||||
// documented 1.0 surface; keep it reachable but off the rendered docs so we
|
// documented 1.0 surface; keep it reachable but off the rendered docs so we
|
||||||
|
|||||||
+10
-4
@@ -660,10 +660,12 @@ mod tests {
|
|||||||
assert!(st.bus_encryption, "cert bus_encryption bit must propagate");
|
assert!(st.bus_encryption, "cert bus_encryption bit must propagate");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No content cert at all but bus_encryption can't be read → version
|
/// No content cert at all → version defaults to UHD (major 2), matching
|
||||||
/// defaults to 1 (encrypt.rs: `None => 1`). bus_encryption false.
|
/// `read_aacs_version` so the scanned `AacsState.version` and the out-of-band
|
||||||
|
/// fetch agree on the Unit_Key_RO stride (audit #4: a wrong BD-vs-UHD guess
|
||||||
|
/// mis-parses unit keys). bus_encryption false (unreadable → off).
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_vid_only_no_cert_defaults_version_1() {
|
fn resolve_vid_only_no_cert_defaults_version_uhd() {
|
||||||
let mut disc = MemDisc::new();
|
let mut disc = MemDisc::new();
|
||||||
let udf = build_aacs_fs(
|
let udf = build_aacs_fs(
|
||||||
&mut disc,
|
&mut disc,
|
||||||
@@ -675,7 +677,11 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
||||||
assert_eq!(st.version, 1, "no cert → default version 1");
|
assert_eq!(
|
||||||
|
st.version,
|
||||||
|
aacs::AACS_MAJOR_UHD,
|
||||||
|
"no cert → default UHD (major 2)"
|
||||||
|
);
|
||||||
assert!(!st.bus_encryption);
|
assert!(!st.bus_encryption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -227,7 +227,12 @@ impl DiscStream {
|
|||||||
|
|
||||||
// CSS/unencrypted content needs a decrypting wrapper to yield plaintext
|
// CSS/unencrypted content needs a decrypting wrapper to yield plaintext
|
||||||
// VOB bytes before the AC-3 sub-stream probe can read real `acmod`s.
|
// VOB bytes before the AC-3 sub-stream probe can read real `acmod`s.
|
||||||
let mut reader = DecryptingSectorSource::new(reader, decrypt_keys.clone());
|
// MUX path: tolerate decrypt loss — conceal an undecryptable unit (NULL TS
|
||||||
|
// fill) + tally + log rather than abort the stream (P3). DiscStream is a
|
||||||
|
// decode/mux stream (live-drive single-pass / direct), never the
|
||||||
|
// ciphertext-preserving sweep, so concealment is always correct here.
|
||||||
|
let mut reader =
|
||||||
|
DecryptingSectorSource::new(reader, decrypt_keys.clone()).tolerate_decrypt_loss();
|
||||||
|
|
||||||
// Wrong-substream fix (Silence-of-the-Lambs): re-route the title's
|
// Wrong-substream fix (Silence-of-the-Lambs): re-route the title's
|
||||||
// declared AC-3 audio onto the physically-correct `0x8x` sub-streams by
|
// declared AC-3 audio onto the physically-correct `0x8x` sub-streams by
|
||||||
|
|||||||
+6
-1
@@ -629,8 +629,13 @@ pub fn build_iso_pipeline<S: SectorSource + Send + 'static>(
|
|||||||
crate::decrypt::DecryptKeys::Aacs { .. } => 3,
|
crate::decrypt::DecryptKeys::Aacs { .. } => 3,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
};
|
};
|
||||||
|
// MUX path: tolerate decrypt loss. An undecryptable content unit is concealed
|
||||||
|
// (NULL TS fill) + tallied + logged, never an abort — decrypt-verify is a RIP
|
||||||
|
// gate, not a mux gate (P3). The rip's own read paths keep their fail-loud
|
||||||
|
// decorator; only this mux pipeline opts in.
|
||||||
let mut decrypting =
|
let mut decrypting =
|
||||||
crate::sector::DecryptingSectorSource::new(Box::new(reader) as Box<dyn SectorSource>, keys);
|
crate::sector::DecryptingSectorSource::new(Box::new(reader) as Box<dyn SectorSource>, keys)
|
||||||
|
.tolerate_decrypt_loss();
|
||||||
// Install the fresh-key-on-failure callback (if any) so a unit no held key
|
// Install the fresh-key-on-failure callback (if any) so a unit no held key
|
||||||
// decrypts is re-tried via the application's key source before being counted
|
// decrypts is re-tried via the application's key source before being counted
|
||||||
// as loss.
|
// as loss.
|
||||||
|
|||||||
@@ -143,6 +143,20 @@ pub struct DecryptingSectorSource<S: SectorSource> {
|
|||||||
/// Reused scratch buffer for verify-only decrypt checks — avoids a per-read
|
/// Reused scratch buffer for verify-only decrypt checks — avoids a per-read
|
||||||
/// allocation on the sweep's hot path. Grown on demand, never shrunk.
|
/// allocation on the sweep's hot path. Grown on demand, never shrunk.
|
||||||
scratch: Vec<u8>,
|
scratch: Vec<u8>,
|
||||||
|
/// MUX loss-concealment switch (P3 / Edit-2). When `true`, a content unit
|
||||||
|
/// that genuinely won't decrypt is NOT a read failure: it is overwritten with
|
||||||
|
/// valid NULL TS packets ([`crate::aacs::fill_null_ts_unit`]), tallied into
|
||||||
|
/// [`decrypt_dropped`](Self::decrypt_dropped), logged loud with its LBA, and
|
||||||
|
/// the read returns `Ok` so the mux KEEPS GOING (it can never abort over an
|
||||||
|
/// undecryptable unit). This is the spec's "decrypt-verify is a RIP gate, not
|
||||||
|
/// a MUX gate": the rip path leaves this `false` (default) and fails loud via
|
||||||
|
/// [`DECRYPT_VERIFY_READ`] so its read-error recovery re-reads the disc; only
|
||||||
|
/// the mux read path opts in. Ciphertext is NEVER passed downstream either
|
||||||
|
/// way — fail-loud re-reads it, conceal replaces it with null packets.
|
||||||
|
///
|
||||||
|
/// Mutually meaningful only with `!verify_only` (the in-place decrypt path
|
||||||
|
/// the mux uses); a verify-only sweep keeps the rip's fail-loud contract.
|
||||||
|
tolerate_decrypt_loss: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: SectorSource> DecryptingSectorSource<S> {
|
impl<S: SectorSource> DecryptingSectorSource<S> {
|
||||||
@@ -164,9 +178,20 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
verify_only: false,
|
verify_only: false,
|
||||||
content_ranges: None,
|
content_ranges: None,
|
||||||
scratch: Vec::new(),
|
scratch: Vec::new(),
|
||||||
|
tolerate_decrypt_loss: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opt into MUX loss-concealment: an undecryptable content unit is concealed
|
||||||
|
/// (filled with NULL TS packets), tallied, logged loud, and the read still
|
||||||
|
/// succeeds — the mux never aborts over it. See
|
||||||
|
/// [`tolerate_decrypt_loss`](Self::tolerate_decrypt_loss). The rip path must
|
||||||
|
/// NOT set this (it relies on fail-loud read-error recovery).
|
||||||
|
pub fn tolerate_decrypt_loss(mut self) -> Self {
|
||||||
|
self.tolerate_decrypt_loss = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Restrict decrypt/verify to the disc's encrypted-content extents
|
/// Restrict decrypt/verify to the disc's encrypted-content extents
|
||||||
/// (sorted/merged `(start_lba, sector_count)` — see
|
/// (sorted/merged `(start_lba, sector_count)` — see
|
||||||
/// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)).
|
/// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)).
|
||||||
@@ -533,6 +558,46 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
if dropped > 0 {
|
if dropped > 0 {
|
||||||
self.decrypt_dropped
|
self.decrypt_dropped
|
||||||
.fetch_add(dropped as u64, Ordering::Relaxed);
|
.fetch_add(dropped as u64, Ordering::Relaxed);
|
||||||
|
// MUX CONCEALMENT (P3 / Edit-2): on the mux read path an undecryptable
|
||||||
|
// content unit is NOT a read failure — never abort the mux over it.
|
||||||
|
// Overwrite each still-scrambled in-content unit with valid NULL TS
|
||||||
|
// packets (A2: keeps the demuxer byte-synced; the lost video/audio PID
|
||||||
|
// packets surface as a CC gap the TS assembler already drops a partial
|
||||||
|
// PES on), tally it (done above), log it LOUD with the LBA, and return
|
||||||
|
// Ok so the stream keeps flowing. Verify-only (sweep) is excluded — the
|
||||||
|
// rip stays fail-loud. Ciphertext is never passed downstream: it is
|
||||||
|
// replaced by null packets, not emitted.
|
||||||
|
if self.tolerate_decrypt_loss && !self.verify_only {
|
||||||
|
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
||||||
|
let mut concealed = 0usize;
|
||||||
|
let mut first_lba = lba;
|
||||||
|
for (i, chunk) in buf[..n].chunks_mut(unit_len).enumerate() {
|
||||||
|
if chunk.len() < unit_len {
|
||||||
|
continue; // trailing partial can't be a whole scrambled unit
|
||||||
|
}
|
||||||
|
// A unit still flagged-encrypted + scrambled after the decrypt
|
||||||
|
// pass is the genuinely-undecryptable content. In-content gating
|
||||||
|
// already happened in `decrypt_buf`, which restored only those
|
||||||
|
// units to ciphertext; clear nav passed through clean.
|
||||||
|
if crate::aacs::aacs_unit_needs_decrypt(chunk) {
|
||||||
|
if concealed == 0 {
|
||||||
|
first_lba = lba + (i as u32) * crate::aacs::ALIGNED_UNIT_SECTORS;
|
||||||
|
}
|
||||||
|
crate::aacs::fill_null_ts_unit(chunk);
|
||||||
|
concealed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if concealed > 0 {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::decrypt",
|
||||||
|
lba = first_lba,
|
||||||
|
units = concealed,
|
||||||
|
bytes = dropped,
|
||||||
|
"mux: undecryptable content concealed as NULL TS (loss tallied)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(n);
|
||||||
|
}
|
||||||
// DECRYPT_VERIFY_READ: a unit that SHOULD have decrypted but didn't
|
// DECRYPT_VERIFY_READ: a unit that SHOULD have decrypted but didn't
|
||||||
// means this read did NOT truly succeed — it returned ciphertext the
|
// means this read did NOT truly succeed — it returned ciphertext the
|
||||||
// TS assembler would silently drop. Fail the read loud so the caller's
|
// TS assembler would silently drop. Fail the read loud so the caller's
|
||||||
@@ -1266,6 +1331,113 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MUX CONCEALMENT (P3): with `tolerate_decrypt_loss()` an undecryptable AACS
|
||||||
|
/// content unit must NOT fail the read. Instead the decorator (a) tallies the
|
||||||
|
/// loss, (b) overwrites the unit with valid NULL TS packets (PID 0x1FFF, sync
|
||||||
|
/// 0x47 at the BD-TS stride), and (c) returns `Ok` so the mux keeps going.
|
||||||
|
/// This is the inverse of the fail-loud rip path proven directly above.
|
||||||
|
#[test]
|
||||||
|
fn tolerate_decrypt_loss_conceals_undecryptable_unit_as_null_ts() {
|
||||||
|
let real_key = [0x33u8; 16];
|
||||||
|
let wrong_key = [0x44u8; 16];
|
||||||
|
|
||||||
|
// One unit encrypted under real_key, plus one trailing CLEAR (TS-sync)
|
||||||
|
// unit so we can confirm conceal touches ONLY the undecryptable unit.
|
||||||
|
let enc = encrypt_aacs_unit(&real_key);
|
||||||
|
let mut clear = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
||||||
|
let mut o = 4;
|
||||||
|
while o < clear.len() {
|
||||||
|
clear[o] = 0x47;
|
||||||
|
o += 192;
|
||||||
|
}
|
||||||
|
let mut two_units = enc;
|
||||||
|
two_units.extend_from_slice(&clear);
|
||||||
|
|
||||||
|
struct TwoUnitSource {
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
impl SectorSource for TwoUnitSource {
|
||||||
|
fn capacity_sectors(&self) -> u32 {
|
||||||
|
(self.data.len() / 2048) as u32
|
||||||
|
}
|
||||||
|
fn read_sectors(
|
||||||
|
&mut self,
|
||||||
|
_lba: u32,
|
||||||
|
count: u16,
|
||||||
|
buf: &mut [u8],
|
||||||
|
_recovery: bool,
|
||||||
|
) -> Result<usize> {
|
||||||
|
let bytes = count as usize * 2048;
|
||||||
|
buf[..bytes].copy_from_slice(&self.data[..bytes]);
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut wrapped = DecryptingSectorSource::new(
|
||||||
|
TwoUnitSource { data: two_units },
|
||||||
|
DecryptKeys::Aacs {
|
||||||
|
unit_keys: vec![(0, wrong_key)], // can't open the encrypted unit
|
||||||
|
read_data_key: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.tolerate_decrypt_loss();
|
||||||
|
let loss = wrapped.decrypt_loss();
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; 6 * 2048];
|
||||||
|
// Must SUCCEED (no DecryptFailed) — the mux never aborts on bad decrypt.
|
||||||
|
let n = wrapped
|
||||||
|
.read_sectors(0, 6, &mut buf, false)
|
||||||
|
.expect("tolerate_decrypt_loss must conceal, not error");
|
||||||
|
assert_eq!(n, 6 * 2048);
|
||||||
|
|
||||||
|
// The undecryptable unit is tallied as loss.
|
||||||
|
assert_eq!(
|
||||||
|
loss.load(Ordering::Relaxed),
|
||||||
|
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
||||||
|
"the concealed unit is still counted as loss"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unit 0 is now valid NULL TS packets — sync 0x47 at every 192-byte
|
||||||
|
// stride (offset 4), PID 0x1FFF — and carries no ciphertext.
|
||||||
|
let unit0 = &buf[..crate::aacs::ALIGNED_UNIT_LEN];
|
||||||
|
let mut off = 0;
|
||||||
|
while off + 192 <= unit0.len() {
|
||||||
|
assert_eq!(unit0[off + 4], 0x47, "null packet sync at {off}");
|
||||||
|
assert_eq!(unit0[off + 5] & 0x1F, 0x1F, "PID high bits 0x1FFF");
|
||||||
|
assert_eq!(unit0[off + 6], 0xFF, "PID low byte 0xFF");
|
||||||
|
off += 192;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!crate::aacs::ts_sync_destroyed(unit0),
|
||||||
|
"concealed unit reads as well-formed TS, not scrambled"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unit 1 (clear) passed through untouched.
|
||||||
|
let unit1 = &buf[crate::aacs::ALIGNED_UNIT_LEN..2 * crate::aacs::ALIGNED_UNIT_LEN];
|
||||||
|
assert_eq!(unit1, &clear[..], "the clear unit is left exactly as read");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `fill_null_ts_unit` round-trip: every BD source packet in the unit becomes
|
||||||
|
/// a well-formed TS null packet, and a TS demuxer tracking a real PID sees
|
||||||
|
/// none of them (PID 0x1FFF matches nothing) — the basis for A2 concealment.
|
||||||
|
#[test]
|
||||||
|
fn null_ts_fill_is_well_formed_and_invisible_to_real_pids() {
|
||||||
|
let mut unit = vec![0xAAu8; crate::aacs::ALIGNED_UNIT_LEN];
|
||||||
|
crate::aacs::fill_null_ts_unit(&mut unit);
|
||||||
|
// 32 packets, each sync 0x47, PID 0x1FFF, payload-only CC 0.
|
||||||
|
let mut off = 0;
|
||||||
|
let mut pkts = 0;
|
||||||
|
while off + 192 <= unit.len() {
|
||||||
|
assert_eq!(unit[off + 4], 0x47);
|
||||||
|
let pid = ((unit[off + 5] as u16 & 0x1F) << 8) | unit[off + 6] as u16;
|
||||||
|
assert_eq!(pid, 0x1FFF, "null PID");
|
||||||
|
assert_eq!(unit[off + 7] & 0x30, 0x10, "payload-only");
|
||||||
|
off += 192;
|
||||||
|
pkts += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(pkts, 32, "32 source packets per aligned unit");
|
||||||
|
}
|
||||||
|
|
||||||
/// Fresh-key-on-failure: a unit encrypted under a key NOT in the initial set
|
/// Fresh-key-on-failure: a unit encrypted under a key NOT in the initial set
|
||||||
/// would normally count as decrypt loss. With a [`with_key_fetch`] callback
|
/// would normally count as decrypt loss. With a [`with_key_fetch`] callback
|
||||||
/// that returns that key, the decorator must hand the still-scrambled unit to
|
/// that returns that key, the decorator must hand the still-scrambled unit to
|
||||||
|
|||||||
Reference in New Issue
Block a user