crates/veilvoice-gui/src/decoys.rs
what this file is for · veilvoice-gui · 216 lines · the same file on GitHub
The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.
// SPDX-License-Identifier: GPL-3.0-or-later
//! Decoy vaults: how many there is room for, and the panel that offers them.
//!
//! # What a decoy is worth, said plainly
//!
//! A decoy is a directory that is the same shape as the real vault, holding
//! encrypted nonsense under a key that was thrown away the moment it was
//! written. Somebody who takes the disk sees several vaults and cannot tell
//! which one holds anything: the names say nothing, the sizes are the same, and
//! cracking one yields bytes that parse as nothing, which looks exactly like a
//! wrong passphrase.
//!
//! What that buys is the cost of searching. It does **not** hide the real vault
//! from somebody watching the screen while it is opened, from somebody who has
//! already got into the running program, or from a backup taken before the
//! decoys were made. The panel says all three where the button is, because a
//! defence somebody misjudges is worse than one they do not have.
//!
//! # Why the count is measured
//!
//! A number chosen here would be a guess dressed as advice: nine decoys is
//! careless on a nearly full laptop and timid on a four-terabyte disk. So the
//! count comes from the room actually free where the vault lives, and where the
//! system will not say how much that is, the panel says so instead of quietly
//! inventing a figure.
use egui::{RichText, Ui};
use veilvoice_crypto::studio::Shape;
use crate::theme::palette as p;
/// Never more than this many, however much room there is.
///
/// Past here the work of searching stops rising in any way that matters: an
/// attacker willing to attack thirty-two vaults is willing to attack a hundred,
/// and the folder becomes something its owner cannot look at and understand.
const MOST: usize = 32;
/// What to offer, and on what evidence.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Advice {
/// What one decoy will occupy on the disk, in bytes.
pub each: u64,
/// How many the free space allows, or `None` where it could not be read.
///
/// `None` is a measurement that did not happen, not a measurement of zero,
/// and the panel says which.
pub room: Option<usize>,
/// The most this offers at once.
pub most: usize,
/// What the count starts at.
pub count: usize,
}
/// Work out what to offer from the vault's shape and whatever the disk says.
///
/// Pure, so the arithmetic can be tested without a disk and without a window.
pub fn advise(shape: Shape, free: Option<u64>) -> Advice {
// Never zero: a vault of no recordings is still an index file, so a decoy
// of it still occupies something and the division below is always safe.
let each = shape.bytes_on_disk().max(1);
// `try_from` rather than `as`: on a 32-bit build a count that does not fit
// in a `usize` must come back as the largest one that does, not as whatever
// the low bits of it happen to be. It is capped again below in any case,
// and relying on that would be relying on the wrong thing.
let room = free.map(|free| usize::try_from((free / SHARE) / each).unwrap_or(usize::MAX));
let most = room.unwrap_or(MOST).min(MOST);
Advice {
each,
room,
most,
// From the measurement where there is one. Where there is not, one:
// the smallest thing that is still a decoy, offered as a floor rather
// than as advice, because advice needs a measurement behind it.
count: match room {
Some(room) => room.min(MOST),
None => 1,
},
}
}
/// The panel, under the listing in the Browser.
///
/// Returns how many decoys were asked for, or `None` when nothing was asked.
/// Drawing and doing are separate because making a decoy writes files and the
/// caller owns the vault this is measuring.
pub fn panel(
ui: &mut Ui,
shape: Shape,
free: Option<u64>,
existing: usize,
wanted: &mut usize,
) -> Option<usize> {
let advice = advise(shape, free);
*wanted = (*wanted).clamp(1, advice.most.max(1));
let mut asked = None;
egui::CollapsingHeader::new("Decoy vaults")
.id_salt("studio-decoys")
.show(ui, |ui| {
ui.label(
RichText::new(
"A decoy is a vault whose contents never existed. It is filled with \
encrypted nonsense under a key made here and dropped before this \
finishes, so there is no key to find, to leak, or to be made to hand \
over. Cracked, it yields bytes that parse as nothing.",
)
.color(p::muted())
.small(),
);
ui.add_space(6.0);
ui.label(
RichText::new(format!(
"This folder holds {}. Each new one will take {}.",
counted_vaults(existing),
crate::studio::size(advice.each as usize)
))
.color(p::blue())
.small(),
);
match advice.room {
Some(room) => ui.label(
RichText::new(format!(
"There is room for {room} at a twentieth of the free space, so that \
is what this starts on.",
))
.color(p::muted())
.small(),
),
None => ui.label(
RichText::new(
"This system would not say how much room is free, so the number \
below is a starting point rather than a measurement. Check there \
is room before making many.",
)
.color(p::yellow())
.small(),
),
};
ui.add_space(8.0);
// Room for none is offered as none rather than as one anyway. A
// decoy written onto a disk with nothing left on it is how somebody
// loses the recording they make next.
let possible = advice.most > 0;
ui.add_enabled_ui(possible, |ui| {
ui.horizontal(|ui| {
ui.label("how many ");
ui.add(
egui::Slider::new(wanted, 1..=advice.most.max(1))
.clamping(egui::SliderClamping::Always),
);
});
ui.add_space(6.0);
if ui.button(RichText::new(" make them ").strong()).clicked() {
asked = Some(*wanted);
}
});
if !possible {
ui.label(
RichText::new(
"There is not room here for one, so none is offered. Free some \
space and open this again.",
)
.color(p::red())
.small(),
);
}
ui.add_space(8.0);
ui.label(
RichText::new(
"What this buys is the cost of a search. It does not hide the real \
vault from somebody watching you open it, from something already \
running inside this computer, or from a backup taken before the \
decoys were made.",
)
.color(p::yellow())
.small(),
);
ui.label(
RichText::new(
"Decoys cannot be told from the real vault by looking, which means \
you cannot tell them apart either. Removing one is removing a \
directory you cannot open to check first.",
)
.color(p::muted())
.small(),
);
});
asked
}
/// "one vault" or "four vaults", so the panel does not say "1 vaults".
fn counted_vaults(n: usize) -> String {
match n {
0 => "no vaults".to_string(),
1 => "one vault".to_string(),
n => format!("{n} vaults"),
}
}
#[cfg(test)]
#[path = "decoys/tests.rs"]
mod tests;