Skip to content

Instantly share code, notes, and snippets.

@theepicsnail
Created August 29, 2023 07:52
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/abfbf1461843e6de9929ba94a4f2a958 to your computer and use it in GitHub Desktop.
Save theepicsnail/abfbf1461843e6de9929ba94a4f2a958 to your computer and use it in GitHub Desktop.
Slightly cleaned up version of the tarot/playing card to entropy bits converter.
use rand::seq::SliceRandom;
use std::io::{stdin, stdout, Write};
/// Replaces angle brackets in a format string with corresponding parameters.
///
/// This function takes a format string and a vector of parameters and replaces
/// occurrences of angle brackets with the corresponding parameter value. The format
/// string is split at angle brackets, and even indices represent parts of the string
/// outside angle brackets, while odd indices correspond to the content within angle brackets.
///
/// # Arguments
///
/// * `format_str` - The format string containing angle brackets to be replaced.
/// * `params` - A vector of parameters to replace angle brackets with.
///
/// # Returns
///
/// A new string with angle brackets replaced by the corresponding parameters.
///
/// # Example
///
/// ```
/// let format_str = "<0> - Option a\n<1> - Option b";
/// let params = vec!["13", "52"];
/// let result = replace_angle_brackets(format_str, &params);
/// println!("{}", result); // Output: "13 - Option a\n52 - Option "
/// ```
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>()
}
/// Terminal prefix for clearing the screen and positioning the cursor.
///
/// * `\x1b[2J` - Clear the entire screen.
/// * `\x1b[3J` - Clear scroll-back buffer.
/// * `\x1b[H` - Position cursor at the top-left corner.
const TERM_PREFIX: &str = "\x1b[2J\x1b[3J\x1b[H";
// All the possible inputs that are offered to the user.
// The particular values here do not matter, only that they're unique.
// They don't technically 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",
];
/// Securely prompts the user for input.
/// User input is randomized per input to mitigate key loggers.
///
/// # Arguments
///
/// * `display_format` - Format string for displaying options to the user.
///
/// # Returns
///
/// The format string's value for the user selected option.
///
/// # Example
///
/// ```
/// let display_format = "<0> - Option A\n<1> - Option B";
/// let selected_index = secure_user_input(display_format, &options);
/// println!("Selected index: {}", selected_index);
/// ```
///
/// The user is presented with something like:
/// ```
/// 13 - Option A
/// 47 - Option B
/// ```
///
/// If the user input 47, the value 1 (from the display_format) is returned.
///
fn secure_user_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)
);
// Last line may not have a newline, force a flush.
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;
}
}
}
/// Formats a vector of input indices using corresponding labels.
///
/// # Arguments
///
/// * `inputs` - A reference to a vector of input indices.
/// * `labels` - A slice of labels corresponding to the indices.
///
/// # Returns
///
/// A formatted string where each input index is replaced with its corresponding label,
/// joined by spaces.
///
/// # Panics
///
/// This function will panic if a label is not found for a given input index.
///
/// # Example
///
/// ```
/// let inputs = vec![0, 2, 1];
/// let labels = vec!["Label A", "Label B", "Label C"];
/// let formatted = format_inputs(&inputs, &labels);
/// println!("Formatted: {}", formatted); // Output: "Label A Label C Label B"
/// ```
fn format_inputs(inputs: &Vec<u8>, labels: &[&str]) -> String {
inputs
.iter()
.map(|&index| labels.get(index as usize).unwrap())
.cloned()
.collect::<Vec<&str>>()
.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::PlayingCardInput => {
display_screen(PLAYING_CARD_DISPLAY, &PLAYING_CONFIRM_STRINGS, 25, 128)
}
Screen::Tarot128CardInput => {
display_screen(TAROT_CARD_DISPLAY, &TAROT_CONFIRM_STRINGS, 22, 128)
}
Screen::Tarot256CardInput => {
display_screen(TAROT_CARD_DISPLAY, &TAROT_CONFIRM_STRINGS, 45, 256)
}
Screen::DisplayEntropy(bits) => entropy_screen(bits),
Screen::Exit => break,
}
}
print!("{}", TERM_PREFIX);
}
fn display_screen(
display_format: &str,
confirm_strings: &[&str],
inputs_required: usize,
bits: usize,
) -> Screen {
let deck_size = confirm_strings.len() as u8;
let mut inputs: Vec<u8> = vec![];
loop {
// Generate the screen:
// EXIT and UNDO are replaced with fixed input values that are outside the range of card inputs.
// PROGRESS is replaced with a string like "3 of 45" in the display.
// FINALIZE is replaced with a finalize input if the required number of inputs have been provided.
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("FINALIZE", if is_complete { "<97> - Finalize" } else { "" })
.replace("PROGRESS", &format!("{:?} of {:?}", inputs.len(), inputs_required))
+ &format_inputs(&inputs, confirm_strings)
+ " "; // Space to separate the last of the confirm strings from the user input area.
// Show the user the current screen, get their input.
let num = secure_user_input(&current_display) as u8;
// 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
// If it's invalid we just ignore the input.
let search = num as u8;
if !inputs.contains(&search) {
inputs.push(search);
}
} else if num == 99 { // <99> - Exit
return Screen::Exit;
} else if num == 98 { // <98> - Undo
inputs.pop();
} else if num == 97 && is_complete { // <97> - Finalize
return Screen::DisplayEntropy(compute_entropy(inputs, deck_size, bits));
}
}
}
/*
* Main screen, let the user choose which mode they want to use.
*/
fn main_screen() -> Screen {
match secure_user_input(&format!("{}", MAIN_DISPLAY)) {
99 => Screen::Exit,
0 => Screen::PlayingCardInput,
1 => Screen::Tarot128CardInput,
2 => Screen::Tarot256CardInput,
_ => Screen::MainScreen,
}
}
// Screen for the user to select which variant they want.
const MAIN_DISPLAY: &str = 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
"#;
// 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. 31 in the display string above is Ten of Wands, [31] 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> |
--------+--------+--------+--------+--------+
UNDO (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 {
secure_user_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