# Keypress entropy

A good password is hard to type. Is it a passphrase? My hand is strained from just looking at diceware's suggested passphrase `NinetiethStunnedPurityStumbleDorsalReps` (39 chars, 45 Keypress). pass is not much better with `''~E;I|pWZ)K:\?~=U&q-|8^V`. That's 25 chars, but 17 shift presses on a US layout. Together it's 42 presses, almost as many as the passphrase. It's not a problem for *pass*. It's a manager, and it will remember the passwords for me so that I don't have to type it.

But I still need to type the master password. And it needs to be **good**, with 128 bits of entropy (entropy measures how many choices were made to generate the password) and a glittery wrapper. So I should just take the one *pass* gives me, right?

No, not right. There's no reason I should be mashing shift like crazy. It only doubles the available characters - 1 extra bit of entropy for each. For the 25-characters password, the shift key is responsible for 25 bits of entropy (about 6 characters), but for 17 extra key presses. That's not a good deal at all.

The wrong choice of a password generator hurts multiple times every day, when I type in the various passwords: my computer unlock password, the one to unlock the password manager, another computer, and whenever I need to install some new package or just use *sudo*. That's too much typing to rely on badly generated passwords.

## Generating key presses

But I know how to generate passwords specifically for typing them in. It's to switch focus from characters to key presses. Just like the choice of a character from a given set represents entropy, so does choosing a key from the available ones. So instead building the password by choosing among characters:

> a b c ... ABC ... 1 2 3 ... ! ? , ...

let's choose among keys:

> ` 1 2 ... q w e ... shift ...

Let's forget for a moment about how many thousands of symbols Unicode contains. No password manager generates ⸘, because they *do* typically make a concession to typists by limiting themselves to some version of the ASCII set of characters.

The actual difference between generating characters and keys is that there are about 100 characters the typical US keyboard layout can produce, using about 50 keys. See that 100/50 = 2? That's the extra bit of entropy we're giving up per character. We have to make up for it if we don't want to trade security for less button mashing.

## A password generator

So how do we generate a password that still has our 128 bits of entropy, but isn't as buttony as the naively generated one?

We write our own generator, of course.

Hasword is a short Rust program I wrote for this purpose. (It's listed at the end of this post.) It generates a password, and tells you how many bits of entropy were used to generate it, the length in characters, and the number of key presses.

Here is an example of 128-bits-of-entropy passwords generated naively, from all characters:

```
# cargo run 128
g`{m>|.4->Q1.^(&n7<J
Entropy: 131.39711
Chars: 20
Presses: 30
Modifiers: 10
```

And here's the same, except generated from key presses:

```
# cargo run 128
t i8ep51\:,.9y0me 94;z
Entropy: 129.07819
Chars: 22
Presses: 23
Modifiers: 1
```

Oh no, the lower per-character entropy in the typist's password makes it 2 characters longer than the naive password. But it's a *total win* in the button mashing department: it's 10% longer but it needs only 23/30≈3/4 the key presses without being any less secure!

Now, the only problem left is to commit it to memory…


Here's the source code for the password generator. I use rdrand to block as many ways as possible for the attacker to guess the random numbers, but I do not guarantee that I did anything right here. You use it on your own responsibility!

```
/* COPYING: allowed under GNU GPL v3 or later. 
To build, do `cargo init` and add this to Cargo.toml:
[dependencies]
rand = "0.8"
rdrand = "*"
*/
use rand;
use rand::seq::SliceRandom;
use std::env;
use std::iter::repeat;
use std::str::FromStr;

enum C {
    Char(char),
    Shift,
}

use C::*;

const PLAIN: &[C] = &[
    Char('`'), Char('1'), Char('2'), Char('3'), Char('4'), Char('5'),
    Char('6'), Char('7'), Char('8'), Char('9'), Char('0'), Char('-'),
    Char('='), Char('\\'), Char(']'), Char('['), Char('p'), Char('o'),
    Char('i'), Char('u'), Char('y'), Char('t'), Char('r'), Char('e'),
    Char('w'), Char('q'), Char('a'), Char('s'), Char('d'), Char('f'),
    Char('g'), Char('h'), Char('j'), Char('k'), Char('l'), Char(';'),
    Char('\''), Char('/'), Char('.'), Char(','), Char('m'), Char('n'),
    Char('b'), Char('v'), Char('c'), Char('x'), Char('z'), Char(' '),
    Shift,
];

const SHIFTED: &[char] = &[
    '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '|', '}', '{', 'P',
    'O', 'I', 'U', 'Y', 'T', 'R', 'E', 'W', 'Q', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K',
    'L', ':', '"', '?', '>', '<', 'M', 'N', 'B', 'V', 'C', 'X', 'Z',
];

fn all_chars() -> Vec<char> {
    PLAIN[0..(PLAIN.len() - 1)].iter()
        .map(|c| match c {
            Char(c) => *c,
            _ => panic!(),
        })
        .chain(SHIFTED.iter().map(|c| *c))
        .collect()
}

/// Generates random characters. Considers entropy from each character.
fn chars_generator<R: rand::RngCore>(mut r: &mut R)
    // entropy, keypresses, character
    -> (f32, u8, char)
{
    let chars = all_chars();
    let bits_entropy_char: f32 = (chars.len() as f32).log2();
    
    let c = chars.choose(&mut r).unwrap();
    let presses = 1 + SHIFTED.iter()
        .find(|p| *p == c)
        .is_some() as u8;
    
    (bits_entropy_char, presses, *c)
}

/// Same, but considers entropy from each keypress.
fn typing_generator<R: rand::RngCore>(mut r: &mut R)
    -> (f32, u8, char)
{
    let bits_entropy_plain: f32 = (PLAIN.len() as f32).log2();
    let bits_entropy_shifted: f32 = (SHIFTED.len() as f32).log2();

    match PLAIN.choose(&mut r).unwrap() {
        Char(c) => (bits_entropy_plain, 1u8, *c),
        Shift => (
            bits_entropy_plain + bits_entropy_shifted,
            2,
            *SHIFTED.choose(&mut r).unwrap()
        ),
    }
}

fn main() {
    let needed_entropy = env::args().skip(1).next()
        .and_then(|s| f32::from_str(&s).ok())
        .unwrap_or(128.0);
    
    let mut r = rdrand::RdRand::new().unwrap();

    let gen = typing_generator;
    // uncomment the following line to use naive mode
    //let gen = chars_generator;

    let seq = repeat(()).map(|()| gen(&mut r))
    
    let mut e = 0.0;
    let mut chars = 0;
    let mut presses = 0;
    for (en, p, c) in seq {
        e += en;
        presses += p;
        chars += 1;
        print!("{}", c);
        if e > needed_entropy {
            break;
        }
    }
    println!();
    println!("Entropy: {}", e);
    println!("Chars: {}", chars);
    println!("Presses: {}", presses);
    println!("Modifiers: {}", presses - chars);
}
```

Written on .

Comments

dcz's projects

Thoughts on software and society.

Atom feed