Skip to content

Commit

Permalink
Support multiple char replacements (#1666)
Browse files Browse the repository at this point in the history
* Support multiple char replacements

This adds support for mapping multiple chars to one char in the CAPTCHA
(e.g. `1`, `I`, `l` etc can all map to `i`).

This is done by reworking the `CHAR_REPLACEMENTS` map to read the
"replacement" chars from its keys and "replaced" chars from its values
(now an array).

Additionally, this extracts the font creation/parsing to a (lazy) static
value, so that the font doesn't have to be recreated every time.

* Update src/internet_identity/src/anchor_management/registration.rs

* Clarify replacement
  • Loading branch information
nmattia authored Jun 6, 2023
1 parent ef55a29 commit 18acb46
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 24 deletions.
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/internet_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ rand = { version ="*", default-features = false }

rand_core = { version = "*", default-features = false }
rand_chacha = { version = "*", default-features = false }
captcha = "0.0.9"
captcha = { git = "https://github.com/nmattia/captcha", rev = "9c0d2dd9bf519e255eaa239d9f4e9fdc83f65391" }

# All IC deps
candid = "0.8"
Expand Down
75 changes: 54 additions & 21 deletions src/internet_identity/src/anchor_management/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ use ic_cdk::{call, caller, trap};
use internet_identity_interface::archive::types::{DeviceDataWithoutAlias, Operation};
use internet_identity_interface::internet_identity::types::*;
use rand_core::{RngCore, SeedableRng};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};

#[cfg(not(feature = "dummy_captcha"))]
use captcha::filters::Wave;
use captcha::fonts::Default as DefaultFont;
use captcha::fonts::Font;
use lazy_static::lazy_static;

mod rate_limit;
Expand Down Expand Up @@ -110,8 +112,8 @@ fn random_string<T: RngCore>(rng: &mut T, n: usize) -> String {

#[cfg(feature = "dummy_captcha")]
fn create_captcha<T: RngCore>(rng: T) -> (Base64, String) {
let mut captcha = captcha::RngCaptcha::from_rng(rng);
let captcha = captcha.set_chars(&vec!['a']).add_chars(1).view(96, 48);
let mut captcha = captcha::new_captcha_with(rng, CAPTCHA_FONT.clone());
let captcha = captcha.set_charset(&vec!['a']).add_chars(1).view(96, 48);

let resp = match captcha.as_base64() {
Some(png_base64) => Base64(png_base64),
Expand All @@ -122,32 +124,55 @@ fn create_captcha<T: RngCore>(rng: T) -> (Base64, String) {
}

lazy_static! {
/// Map of problematic characters that are easily mixed up by humans to the "normalized" replacement.
/// I.e. the captcha will never contain any of the characters in the key set and any input provided
/// will be mapped to the matching value.
/// Problematic characters that are easily mixed up by humans to "normalized" replacement.
/// I.e. the captcha will only contain a "replaced" character (values below in map) if the
/// character also appears as a "replacement" (keys below in map). All occurrences of
/// "replaced" characters in the user's challenge result will be replaced with the
/// "replacements".
/// Note: the captcha library already excludes the characters o, O and 0.
static ref CHAR_REPLACEMENTS: HashMap<char, char> = vec![
('C', 'c'),
('l', '1'),
('S', 's'),
('X', 'x'),
('Y', 'y'),
('Z', 'z'),
('P', 'p'),
('W', 'w'),
('J', 'j'),
static ref CHAR_REPLACEMENTS: HashMap<char, Vec<char>> = vec![
('c', vec!['c', 'C']),
('i', vec!['1', 'i', 'l', 'I', 'j']),
('s', vec!['s', 'S']),
('x', vec!['x', 'X']),
('y', vec!['y', 'Y']),
('z', vec!['z', 'Z']),
('p', vec!['p', 'P']),
('w', vec!['w', 'W']),
].into_iter().collect();

/// The font (glyphs) used when creating captchas
static ref CAPTCHA_FONT: DefaultFont = DefaultFont::new();

/// The character set used in CAPTCHA challenges (font charset with replacements)
static ref CHALLENGE_CHARSET: Vec<char> = {
// To get the final charset:
// * Start with all chars supported by the font by default
// * Remove all the chars that will be "replaced"
// * Add (potentially re-add) replacement chars
let mut chars = CAPTCHA_FONT.chars();
{
let dropped: HashSet<char> = CHAR_REPLACEMENTS.values().flat_map(|x| x.clone()).collect();
chars.retain(|c| !dropped.contains(c));
}

{
chars.append(&mut CHAR_REPLACEMENTS.keys().copied().collect());
}

chars
};
}

const CAPTCHA_LENGTH: usize = 5;
#[cfg(not(feature = "dummy_captcha"))]
fn create_captcha<T: RngCore>(rng: T) -> (Base64, String) {
let mut captcha = captcha::RngCaptcha::from_rng(rng);
let mut chars = captcha.supported_chars();
chars.retain(|c| !CHAR_REPLACEMENTS.contains_key(c));
let mut captcha = captcha::new_captcha_with(rng, CAPTCHA_FONT.clone());

let captcha = captcha
.set_chars(&chars)
// Replace the default charset with our more readable charset
.set_charset(&CHALLENGE_CHARSET)
// add some characters
.add_chars(CAPTCHA_LENGTH as u32)
.apply_filter(Wave::new(2.0, 20.0).horizontal())
.apply_filter(Wave::new(2.0, 20.0).vertical())
Expand All @@ -174,7 +199,15 @@ fn check_challenge(res: ChallengeAttempt) -> Result<(), ()> {
let normalized_challenge_res: String = res
.chars
.chars()
.map(|c| *CHAR_REPLACEMENTS.get(&c).unwrap_or(&c))
.map(|c| {
// Apply all replacements
*CHAR_REPLACEMENTS
.iter()
// For each key, see if the char matches any of the values (replaced chars) and if
// so replace with the key itself (replacement char)
.find_map(|(k, v)| if v.contains(&c) { Some(k) } else { None })
.unwrap_or(&c)
})
.collect();

state::inflight_challenges_mut(|inflight_challenges| {
Expand Down

0 comments on commit 18acb46

Please sign in to comment.