ExerciseChallenge
Challenge
Fix five storage schemes
25 minintermediate2–12 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 StarterHints
Hint 1
For each, ask two questions: can a precomputed table be used, and how fast can one password be guessed?
Hint 2
One scheme is reversible. Find the sentence in the requirements that forced it, and answer that instead.
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.
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