Projects · Lesson 25.13

Password

Source
Make a 16-character password from 57 easy-to-read characters, drawing every one from the system's entropy with rejection sampling so that no character is favoured.
You'll need: Parts 1–20 — this project is a checkpoint for Utilities, and leans on Entropy, Propagate and Loop.

This program makes a 16-character password. It is short, but two decisions in it carry real weight, and getting either one wrong produces a password that looks random and is not:

  • Where the randomness comes from. Every character is drawn straight from the operating system's entropy, never from a seeded generator.
  • How a random byte becomes a character. A byte has 256 values and the alphabet has 57 characters. Turning one into the other fairly takes a technique called rejection sampling.

It is a checkpoint for Part 20: Utilities. The result has about 93 bits of strength, and it is different on every run.

How it is put together

PieceIts jobLessons it uses
Alphabet57 characters that cannot be misreadConst, String literal
DrawIndexOne fair index into the alphabet, or an EntropyErrorEntropy, Loop, Propagate, Pointer
ReasonAn EntropyError in wordsMatch expression, Enum
MainSixteen draws into an array, then one line of outputArray, Catch, Writable slice

An alphabet for people

The alphabet leaves out the characters that look like each other in many fonts — l, I, O, 0 and 1:

const Alphabet = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";

A character that can be misread costs more in mistyped passwords than it adds in strength. Each character drawn from 57 adds log₂ 57 ≈ 5.83 bits, so sixteen of them give about 93 bits — far beyond guessing.

Why not a seeded generator?

A generator such as Pcg64Dxsm from Random is predictable by design: the same seed always replays the same numbers. A password made from one is only as secret as its seed, and anyone who learns or guesses the seed can make the same password. So every character here comes from the Entropy package directly, and every request is checked: if the system has no randomness to give, the program stops rather than invent some.

Rejection sampling

Taking byte % 57 is the obvious way to turn a byte into an index, and it is biased. 256 is four whole 57s (228) plus 28 left over, so the first 28 characters of the alphabet would get five chances in 256 while the rest get only four:

Byte valuesbyte % 57 givesEffect
0 to 227every index 4 timesFair
228 to 255indices 0 to 27 once moreBiased towards the first 28 characters

The fix is to throw away a byte from that uneven top and draw again:

func DrawIndex() -> uint ! EntropyError {
    let count = Alphabet.length;
    // The largest multiple of `count` that fits in a byte's 256 values: 228 for 57 characters.
    // Bytes below it split evenly among the characters; bytes from it upwards are rejected.
    let limit = 256 - 256 % count;
    loop {
        var drawn: byte = 0;
        Fill(@drawn as *var opaque, 1)?;
        if (drawn as uint) < limit {
            return (drawn as uint) % count;
        }
    }
}

loop has no condition: it repeats until the return inside it fires. A byte is rejected with probability 28 / 256, about one time in nine, so the loop almost always ends on the first or second try.

Fill writes random bytes into any memory you point it at. It takes an untyped pointer and a length, so @drawn — the address of the one-byte variable — is converted to *var opaque. The ? passes an EntropyError straight to the caller.

Nothing printed until it is complete

Main builds the whole password in an array before printing any of it. A failure halfway therefore leaves nothing on the screen that could be mistaken for a shorter password:

var password: char8[16];
for i in 0..password.length {
    let index = DrawIndex() catch {
        error => {
            PrintLine("no password: {}", Reason(error));
            return 1;
        }
    };
    password[i] = Alphabet[index];
}

PrintLine prints text views, not fixed-size arrays, so the last line passes password[..], a view of the whole array.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A password generator: sixteen characters, each drawn straight from the operating system's
// entropy, from an alphabet chosen so nobody misreads the result.
//
// Two decisions carry the whole program.
//
// Where the randomness comes from. A seeded generator such as `Pcg64Dxsm` is predictable by
// design, so a password made from one is only as secret as its seed. Here every character comes
// from the Entropy package directly, and every request is checked: if the system has no
// randomness to give, the program stops rather than invent some.
//
// How a random byte becomes a character. A byte has 256 values and the alphabet has 57
// characters, and 57 does not divide 256. Taking `byte % 57` would make the first 28 characters
// (256 - 4 * 57 of them) a little more likely than the rest. So a byte from the uneven top of the
// range is thrown away and another is drawn: rejection sampling. Every character is then exactly
// as likely as every other.
//
// The result has 16 * log2(57), about 93 bits of strength. The output differs on every run.
import Entropy::{ EntropyError, Fill };
import Io::PrintLine;

// Lowercase, uppercase and digits, without the look-alikes l, I, O, 0 and 1. A character that
// can be misread costs more in mistyped passwords than it adds in strength.
const Alphabet = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";

// An index into the alphabet, every one equally likely, or why no entropy was available.
func DrawIndex() -> uint ! EntropyError {
    let count = Alphabet.length;
    // The largest multiple of `count` that fits in a byte's 256 values: 228 for 57 characters.
    // Bytes below it split evenly among the characters; bytes from it upwards are rejected.
    let limit = 256 - 256 % count;
    loop {
        var drawn: byte = 0;
        Fill(@drawn as *var opaque, 1)?;
        if (drawn as uint) < limit {
            return (drawn as uint) % count;
        }
    }
}

func Reason(error: EntropyError) -> char8[..] {
    return match error {
        EntropyError::Unsupported => "this system has no entropy source",
        EntropyError::Interrupted => "the request was interrupted",
        EntropyError::Failed => "the system refused",
        EntropyError::TooLarge => "too many bytes in one request"
    };
}

func Main() -> int {
    PrintLine("alphabet  {} ({} characters)", Alphabet, Alphabet.length);

    // The password is built in full before any of it is printed, so a failure halfway leaves
    // nothing on the screen that could be mistaken for a shorter password.
    var password: char8[16];
    for i in 0..password.length {
        let index = DrawIndex() catch {
            error => {
                PrintLine("no password: {}", Reason(error));
                return 1;
            }
        };
        password[i] = Alphabet[index];
    }

    // `PrintLine` prints text views, not fixed-size arrays, so `password[..]` views the whole
    // array. The view is writable, because the array is `var`, and it prints just the same.
    PrintLine("password  {}", password[..]);
    return 0;
}

Besides Io, its Rux.toml lists Entropy under [Dependencies].

Run it

cd Examples/Projects/Password
rux run
alphabet  abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789 (57 characters)
password  3negVXPww5u2Nzh4

This is a sample: the password is different on every run.

Common mistakes

Printing the array itself.
PrintLine("password {}", password); is refused: error: argument 2 to 'PrintLine' has type 'char8[16]', but variadic parameter 'args' requires 'Display'. A fixed-size array is not text to PrintLine; a view of it, password[..], is.
Ignoring a failed request for entropy.
Fill returns ! EntropyError. Drop the ? and the call is refused: error: fallible result of type '! EntropyError' is discarded. Here that is a safety feature: a password generator that carried on after its randomness failed would print something that only looks like a password.
byte % count without rejection.
It compiles, it runs, and the passwords look random. The bias is small — 5 chances in 256 instead of 4 for some characters — which is exactly why it goes unnoticed. Tests cannot see it in one password; only the arithmetic shows it.

Try it yourself

  1. Let the length be a constant, const Length = 20;, and print the strength in bits next to the password. (Math has Log2.)
  2. Add a few symbols such as -, _ and ! to the alphabet. DrawIndex needs no change at all — why not?
  3. Print five passwords, one per line, as a menu to choose from.
  4. Demand at least one digit: after drawing, check the password and draw it again if it has none. Does that change the strength?

Learn more

  • Entropy — Fill and EntropyError
  • Random — why a seeded generator is the wrong tool here
  • Loop — repeating until a return or break
  • Next project: Launch