Skip to content

Instantly share code, notes, and snippets.

@theepicsnail
Created August 29, 2023 04:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save theepicsnail/ae8e530c62fe118c9cb39905e2207030 to your computer and use it in GitHub Desktop.
Save theepicsnail/ae8e530c62fe118c9cb39905e2207030 to your computer and use it in GitHub Desktop.
First pass at a CLI for querying inputs from users and outputting entropy.
// Needs cleaned up/documented. But works.
use rand::seq::SliceRandom;
use std::io::{stdin, stdout, Write};
/* This program will convert a shuffled deck into a binary entropy string. */
/*
More or less format!, but accepts:
non-compile-time format strings
vec<str> params
Doesn't support full formatting, only {idx}.
Will panic if format_str contains indexes out of params bounds.
*/
fn replace_angle_brackets(format_str: impl AsRef<str>, params: &Vec<&str>) -> String {
format_str
.as_ref()
.split(|c| c == '<' || c == '>')
.enumerate()
.map(|(index, part)| {
match index % 2 {
0 => part,
1 => params[part.parse::<usize>().unwrap()],
_ => "", // Unreachable, but for completeness
}
})
.collect::<String>()
}
// \x1b[2J - Clear visible screen
// \x1b[3J - Clear scroll back
// \x1b[H - Home the cursor back to top left
const TERM_PREFIX: &str = "\x1b[2J\x1b[3J\x1b[H";
// All the possible inputs that are offered.
// The particular values here do not matter, only that they're unique.
// I don't think they need to be the same length from a security standpoint, but from a UI standpoint
// if they are variable width, it messes up the ui.
const POSSIBLE_INPUTS: [&str; 100] = [
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15",
"16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31",
"32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47",
"48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63",
"64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95",
"96", "97", "98", "99",
];
/*
Show the user a display, block until they input something then return that.
This uses a randomized mapping of possible inputs.
*/
fn get_input(display_format: &String) -> usize {
let mut perm: Vec<&str> = POSSIBLE_INPUTS.to_vec();
loop {
perm.shuffle(&mut rand::thread_rng());
print!(
"{}{}",
TERM_PREFIX,
replace_angle_brackets(display_format, &perm)
);
let _ = stdout().flush();
let mut input: String = String::new();
stdin().read_line(&mut input).expect("Failed to read line");
let trimmed = input.trim();
if let Some(index) = perm.iter().position(|&s| s == trimmed) {
return index;
}
}
}
fn format_inputs(inputs: &Vec<u8>, labels: &[&str]) -> String {
let strings: Vec<&str> = inputs
.iter()
.map(|&index| labels.get(index as usize).unwrap_or(&"XX"))
.cloned()
.collect();
if strings.len() > 27 {
let (line1, line2) = strings.split_at(27);
format!("{}\n{}", line1.join(" "), line2.join(" "))
} else {
strings.join(" ")
}
}
enum Screen {
MainScreen,
PlayingCardInput,
Tarot128CardInput,
Tarot256CardInput,
DisplayEntropy(String),
Exit,
}
fn main() {
let mut screen = Screen::MainScreen;
loop {
screen = match screen {
Screen::MainScreen => main_screen(),
Screen::Tarot128CardInput => {
collect_entropy(TAROT_CARD_DISPLAY, &TAROT_CONFIRM_STRINGS, 22, 128)
}
Screen::PlayingCardInput => {
collect_entropy(PLAYING_CARD_DISPLAY, &PLAYING_CONFIRM_STRINGS, 25, 128)
}
Screen::Tarot256CardInput => {
collect_entropy(TAROT_CARD_DISPLAY, &TAROT_CONFIRM_STRINGS, 45, 256)
}
Screen::DisplayEntropy(bits) => entropy_screen(bits),
Screen::Exit => break,
}
}
print!("{}", TERM_PREFIX);
}
fn collect_entropy(
display_format: &str,
confirm_strings: &[&str],
inputs_required: usize,
bits: usize,
) -> Screen {
let deck_size = confirm_strings.len();
let mut inputs: Vec<u8> = vec![];
loop {
// Generate the screen.
let progress = format!("{:?} of {:?}", inputs.len(), inputs_required);
let previous_inputs = &format_inputs(&inputs, confirm_strings);
let is_complete = inputs.len() == inputs_required;
// Update the display string to show progress, and if applicable, the code to finish.
let current_display = display_format
.replace("EXIT", "<99> - Exit")
.replace("UNDO", "<98> - Undo")
.replace("PROGRESS", &progress)
.replace("FINALIZE", if is_complete { "<97> - Finalize" } else { "" })
+ previous_inputs;
// Show the user the current screen, get their input.
let num: usize = get_input(&current_display);
// Handle user's input:
if num <= deck_size && !is_complete {
// A valid input is:
// In the range is [0,deck_size]
// We haven't collected the total inputs yet
// It's not a repeat of a previous input
let search = num as u8;
if !inputs.contains(&search) {
inputs.push(search);
}
} else if num == 99 {
return Screen::Exit;
} else if num == 98 {
inputs.pop();
} else if num == 97 && is_complete {
// Finalize
return Screen::DisplayEntropy(compute_entropy(inputs, deck_size as u8, bits));
}
}
}
/*
* Main screen, let the user choose which mode they want to use.
*/
fn main_screen() -> Screen {
match get_input(&format!(
"{}",
r#"
<0> - Use a 52 playing card deck - 128 bits
<1> - Use a 78 tarot card deck - 128 bits
<2> - Use a 78 tarot card deck - 256 bits
<99> - Exit
Enter one of the corresponding numbers to choose
"#
)) {
99 => Screen::Exit,
//0 => Screen::PlayingCardInput,
0 => Screen::PlayingCardInput,
1 => Screen::Tarot128CardInput,
2 => Screen::Tarot256CardInput,
_ => Screen::MainScreen,
}
}
// Screen to display to users for inputting a tarot card
// Format numbers are replaced with a random number for user input
// That number is then converted back to the format number after entry.
const TAROT_CARD_DISPLAY: &str = r#" <0> - Fool(0) |Wands |Swords |Cups |Pentacles
<1> - Magician(1) -------+-------+-------+-------+-------
<2> - High Priestess(2) Ace |<22> |<36> |<50> |<64>
<3> - Empress(3) 2 |<23> |<37> |<51> |<65>
<4> - Emperor(4) 3 |<24> |<38> |<52> |<66>
<5> - Hierophant(5) 4 |<25> |<39> |<53> |<67>
<6> - Lovers(6) -------+-------+-------+-------+-------
<7> - Chariot(7) 5 |<26> |<40> |<54> |<68>
<8> - Strength(8) 6 |<27> |<41> |<55> |<69>
<9> - Hermit(9) 7 |<28> |<42> |<56> |<70>
<10> - Fortune(10) 8 |<29> |<43> |<57> |<71>
<11> - Justice(11) -------+-------+-------+-------+-------
<12> - Hanged Man(12) 9 |<30> |<44> |<58> |<72>
<13> - Death(13) Ten |<31> |<45> |<59> |<73>
<14> - Temperance(14) Page |<32> |<46> |<60> |<74>
<15> - Devil(15) kNight |<33> |<47> |<61> |<75>
<16> - Tower(16) -------+-------+-------+-------+-------
<17> - Star(17) Queen |<34> |<48> |<62> |<76>
<18> - Moon(18) King |<35> |<49> |<63> |<77>
<19> - Sun(19)
<20> - Judgment(20) UNDO PROGRESS
<21> - World(21) EXIT FINALIZE
"#;
// When showing what the user has entered, use these strings:
// E.g. 58 in the display string above is Ten of Wands, [58] here is "TW"
const TAROT_CONFIRM_STRINGS: [&str; 78] = [
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", //
"11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", //
"AW", "2W", "3W", "4W", "5W", "6W", "7W", "8W", "9W", "TW", "PW", "NW", "QW", "KW", //
"AS", "2S", "3S", "4S", "5S", "6S", "7S", "8S", "9S", "TS", "PS", "NS", "QS", "KS", //
"AC", "2C", "3C", "4C", "5C", "6C", "7C", "8C", "9C", "TC", "PC", "NC", "QC", "KC", //
"AP", "2P", "3P", "4P", "5P", "6P", "7P", "8P", "9P", "TP", "PP", "NP", "QP", "KP", //
];
const PLAYING_CARD_DISPLAY: &str = r#"
| Clubs |Diamonds| Hearts | Spades |
--------+--------+--------+--------+--------+
Ace | <0> | <13> | <26> | <39> |
2 | <1> | <14> | <27> | <40> |
3 | <2> | <15> | <28> | <41> |
--------+--------+--------+--------+--------+
4 | <3> | <16> | <29> | <42> |
5 | <4> | <17> | <30> | <43> |
6 | <5> | <18> | <31> | <44> |
--------+--------+--------+--------+--------+
7 | <6> | <19> | <32> | <45> |
8 | <7> | <20> | <33> | <46> |
9 | <8> | <21> | <34> | <47> |
--------+--------+--------+--------+--------+
Ten | <9> | <22> | <35> | <48> |
Jack | <10> | <23> | <36> | <49> |
Queen | <11> | <24> | <37> | <50> |
King | <12> | <25> | <38> | <51> |
--------+--------+--------+--------+--------+
<99> - Back (PROGRESS)
FINALIZE
"#;
const PLAYING_CONFIRM_STRINGS: [&str; 52] = [
"AC", "2C", "3C", "4C", "5C", "6C", "7C", "8C", "9C", "TC", "JC", "QC", "KC", //
"AD", "2D", "3D", "4D", "5D", "6D", "7D", "8D", "9D", "TD", "JD", "QD", "KD", //
"AH", "2H", "3H", "4H", "5H", "6H", "7H", "8H", "9H", "TH", "JH", "QH", "KH", //
"AS", "2S", "3S", "4S", "5S", "6S", "7S", "8S", "9S", "TS", "JS", "QS", "KS", //
];
fn entropy_screen(bits: String) -> Screen {
get_input(&format!("Exit <00>\nBits: {}\n", &bits));
return Screen::Exit;
}
fn compute_entropy(values: Vec<u8>, max: u8, bits: usize) -> String {
let mut bigint: Vec<u8> = vec![];
let mut possible: Vec<u8> = (0..=max).collect();
for value in values {
if let Some(index) = possible.iter().position(|&v| v == value) {
big_multiply_add(&mut bigint, possible.len() as u8, index as u8);
possible.remove(index);
} else {
panic!("Entropy computation failed.");
}
}
let binary = bigint
.iter()
.rev()
.map(|byte| format!("{:08b}", byte))
.collect::<Vec<String>>()
.concat();
if binary.len() >= bits {
format!("{}", &binary[binary.len() - bits..])
} else {
let padding = "0".repeat(bits - binary.len());
format!("{}{}", padding, binary)
}
}
/*
Big-int MAC/MAD function.
https://en.wikipedia.org/wiki/Multiply%E2%80%93accumulate_operation
We're storing unsigned monotonic numbers in base 256 using a Vec<u8>.
We never multiply or add more than a single u8 at a time, so this lets us do linear-time MAC operations.
This will resize the bigint as necessary.
Because this bigint is unsigned, we do not try to support negatives.
Because this is monotonic we do not try to support subtraction or division.
*/
fn big_multiply_add(base256_bigint: &mut Vec<u8>, multiplier: u8, add: u8) {
let mut carry: u8 = add;
base256_bigint.iter_mut().for_each(|digit| {
let product = u16::from(*digit) * u16::from(multiplier) + u16::from(carry);
*digit = product as u8;
carry = (product >> 8) as u8;
});
if carry > 0 {
base256_bigint.push(carry);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment