Challenge

Fix five storage schemes

25 minintermediate212 yrs

Edge cases. You have to reason, and two valid fixes differ.

What this teaches

  • Salting and slowness are separate controls, and both are required
  • Encryption is the wrong tool because it is reversible
  • Iterating a fast hash is not a work factor
  • A global salt is not a salt
  • One scheme is correct and still fails for a reason outside the hash

Starter

Starter.java
import java.util.*;

/**
 * Challenge: five password schemes. One is nearly right.
 *
 * For each, answer two questions before changing anything: can a precomputed
 * table be used against it, and how fast can a single password be guessed?
 * Those two questions separate the salting problem from the speed problem,
 * and most wrong answers fix one and declare victory.
 */
public class Starter {

    static String sha256(String v) throws Exception {
        var md = java.security.MessageDigest.getInstance("SHA-256");
        return HexFormat.of().formatHex(md.digest(v.getBytes("UTF-8")));
    }

    /* ─────────── A ─────────── */
    static String schemeA(String password) throws Exception {
        return sha256(password);
    }

    /* ─────────── B ─────────── */
    static final String GLOBAL_SALT = "s3cr3t-application-salt";
    static String schemeB(String password) throws Exception {
        return sha256(GLOBAL_SALT + password);
    }

    /* ─────────── C ─────────── */
    static String schemeC(String password) throws Exception {
        String h = password;
        for (int i = 0; i < 1000; i++) h = sha256(h);
        return h;
    }

    /* ─────────── D ─────────── */
    static String schemeD(String password, String key) {
        // "Encrypted so support can recover it if a user forgets."
        StringBuilder out = new StringBuilder();
        for (int i = 0; i < password.length(); i++) {
            out.append((char) (password.charAt(i) ^ key.charAt(i % key.length())));
        }
        return HexFormat.of().formatHex(out.toString().getBytes());
    }

    /* ─────────── E ─────────── */
    record User(String name, String salt, String hash) { }

    static User schemeE(String name, String password) throws Exception {
        byte[] salt = new byte[16];
        java.security.SecureRandom.getInstanceStrong().nextBytes(salt);
        var spec = new javax.crypto.spec.PBEKeySpec(
            password.toCharArray(), salt, 210_000, 256);
        byte[] key = javax.crypto.SecretKeyFactory
            .getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
        User user = new User(name, HexFormat.of().formatHex(salt), HexFormat.of().formatHex(key));
        System.out.println("registered: " + user);      // <-- look here
        return user;
    }

    public static void main(String[] args) throws Exception {
        System.out.println("A : " + schemeA("hunter2").substring(0, 24) + "...");
        System.out.println("B : " + schemeB("hunter2").substring(0, 24) + "...");
        System.out.println("C : " + schemeC("hunter2").substring(0, 24) + "...");
        System.out.println("D : " + schemeD("hunter2", "key123"));
        schemeE("ana", "hunter2");

        // TODO 1: for each scheme, answer the two questions.
        //
        //        precomputed table usable?   guesses per second?
        //   A :  ______________________      ___________________
        //   B :  ______________________      ___________________
        //   C :  ______________________      ___________________
        //   D :  ______________________      ___________________
        //   E :  ______________________      ___________________

        // TODO 2: B has a global salt. Say precisely what it defeats and what
        // it does not, and why it is not a per-user salt.

        // TODO 3: C iterates SHA-256 a thousand times. Measure it against
        // PBKDF2 at 210,000 rounds. Then say why "just iterate more" is still
        // not the same as using a KDF — the answer involves who designed the
        // construction and who reviewed it.

        // TODO 4: D is reversible by design, because a requirement said
        // support must recover passwords. Write the two-sentence reply that
        // answers the requirement instead of implementing it.

        // TODO 5: E hashes correctly — right algorithm, right cost, unique
        // salt — and still leaks every password. Find the defect. It is one
        // line and has nothing to do with cryptography.

        // TODO 6: rank A to E from worst to best, and say which single change
        // would improve each one the most.
    }
}

Run it locally:

cd exercises/java/authentication/password-storage/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    For each, ask two questions: can a precomputed table be used, and how fast can one password be guessed?

  2. Hint 2

    One scheme is reversible. Find the sentence in the requirements that forced it, and answer that instead.

  3. Hint 3

    One applies SHA-256 a thousand times. Compare that to 210,000 rounds of a real KDF and say why it is not the same idea.

  4. Hint 4

    The last one hashes correctly and leaks anyway. Look outside the hash function.

Done when

  • Each of the five has a named defect and a specific fix
  • The reversible one is answered by changing the requirement, not the code
  • You can say why iterating SHA-256 is not equivalent to a KDF
  • The correct-but-leaking one is identified and its real defect named

← Back to How should a password be stored?