Say It Out Loud: Why Pronounceable Passwords Are a Hidden Risk
A hands-on Java and Quarkus tutorial for building smarter password strength checks that detect what traditional meters miss.
Most password strength meters measure entropy, character diversity, and length.
But they miss something obvious: how easy it is to say your password out loud.
In this tutorial, you’ll build a Quarkus application that detects pronounceable passwords using Apache Commons Codec’s phonetic algorithms. The goal: penalize passwords that are easy to communicate verbally and therefore easy to steal through social engineering.
Why Pronunciation Matters
Security isn’t just about math. It’s also about psychology and human behavior.
Attackers know that many users pick passwords they can say and remember. That opens several attack vectors:
Phone dictation attacks – “It’s ‘echo lima india tango echo’”
Shoulder surfing – Easy to recall what you hear or see
Social engineering – Users may reveal pronounceable passwords over a call
Phishing – Phonetically simple passwords are easier to manipulate
Optimized brute-force attacks – Attackers start with pronounceable wordlists
So we’ll create a Quarkus-based REST API that evaluates password strength with an additional dimension: pronounceability.
Prerequisites
Make sure you have:
Java 17+
Apache Maven 3.9+
Quarkus CLI (or use Maven directly)
Basic knowledge of REST and JSON
Bootstrap the Project
Check out the repository or create a new Quarkus application:
quarkus create app com.example:password-strength-meter \
--extension="rest-jackson"
cd password-strength-meterAdd Apache Commons Codec to your pom.xml:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.20.0</version>
</dependency>Build the Core Logic
We’ll create three analyzers: syllables, phonetic similarity, and segment pronounceability, then combine them in a scoring engine.
4.1 Syllable Analysis
Create src/main/java/com/example/analysis/SyllableAnalyzer.java:
package com.example.analysis;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SyllableAnalyzer {
private static final Pattern SYLLABLE_PATTERN = Pattern
.compile(”[bcdfghjklmnpqrstvwxyz]*[aeiou]+[bcdfghjklmnpqrstvwxyz]*”, Pattern.CASE_INSENSITIVE);
public static int countSyllables(String password) {
String lettersOnly = password.replaceAll(”[^a-zA-Z]”, “ “);
String[] words = lettersOnly.split(”\\s+”);
int total = 0;
for (String word : words) {
if (word.length() < 2)
continue;
Matcher matcher = SYLLABLE_PATTERN.matcher(word);
while (matcher.find())
total++;
}
return total;
}
public static double getSyllableDensity(String password) {
int syllables = countSyllables(password);
int letters = password.replaceAll(”[^a-zA-Z]”, “”).length();
return letters == 0 ? 0.0 : (double) syllables / letters;
}
public static boolean hasTongueTwisters(String password) {
return password.matches(”.*[bcdfghjklmnpqrstvwxyz]{4,}.*”) ||
password.matches(”.*[aeiou]{3,}.*”);
}
}Phonetic Matching
Next, create PhoneticMatcher.java in the same package:
package com.example.analysis;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.codec.language.Metaphone;
import org.apache.commons.codec.language.Soundex;
public class PhoneticMatcher {
private final Soundex soundex = new Soundex();
private final Metaphone metaphone = new Metaphone();
private static final Map<String, Set<String>> WEAK_PATTERNS = loadWeakPatterns();
private static Map<String, Set<String>> loadWeakPatterns() {
String[] weak = { “password”, “admin”, “welcome”, “letmein”, “sunshine” };
Map<String, Set<String>> map = new HashMap<>();
Soundex sx = new Soundex();
Metaphone mp = new Metaphone();
for (String w : weak) {
Set<String> encodings = new HashSet<>();
encodings.add(sx.encode(w));
encodings.add(mp.encode(w));
map.put(w, encodings);
}
return map;
}
public Result check(String password) {
String clean = password.replaceAll(”[^a-zA-Z]”, “”);
if (clean.length() < 3)
return new Result(false, null, 0);
String sound = soundex.encode(clean);
String meta = metaphone.encode(clean);
for (var entry : WEAK_PATTERNS.entrySet()) {
if (entry.getValue().contains(sound) || entry.getValue().contains(meta)) {
int similarity = similarity(clean, entry.getKey());
return new Result(true, entry.getKey(), similarity);
}
}
return new Result(false, null, 0);
}
private int similarity(String s1, String s2) {
int[][] dp = new int[s1.length() + 1][s2.length() + 1];
for (int i = 0; i <= s1.length(); i++)
dp[i][0] = i;
for (int j = 0; j <= s2.length(); j++)
dp[0][j] = j;
for (int i = 1; i <= s1.length(); i++) {
for (int j = 1; j <= s2.length(); j++) {
int cost = s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1;
dp[i][j] = Math.min(Math.min(
dp[i - 1][j] + 1, dp[i][j - 1] + 1),
dp[i - 1][j - 1] + cost);
}
}
int distance = dp[s1.length()][s2.length()];
int max = Math.max(s1.length(), s2.length());
return (int) ((1.0 - (double) distance / max) * 100);
}
public record Result(boolean match, String pattern, int similarity) {
}
}Segment Analyzer
Create SegmentAnalyzer.java:
package com.example.analysis;
import java.util.ArrayList;
import java.util.List;
public class SegmentAnalyzer {
public static double pronounceability(String password) {
List<String> segs = extract(password);
if (segs.isEmpty())
return 0.0;
int pron = 0;
for (String s : segs)
if (isPronounceable(s))
pron++;
return (double) pron / segs.size();
}
private static List<String> extract(String pw) {
List<String> segs = new ArrayList<>();
String[] parts = pw.split(”[^a-zA-Z]+”);
for (String p : parts)
if (p.length() >= 3)
segs.add(p);
return segs;
}
private static boolean isPronounceable(String seg) {
boolean hasV = seg.matches(”.*[aeiou].*”);
boolean hasC = seg.matches(”.*[bcdfghjklmnpqrstvwxyz].*”);
if (!hasV || !hasC)
return false;
int alt = 0;
boolean lastV = isVowel(seg.charAt(0));
for (int i = 1; i < seg.length(); i++) {
boolean v = isVowel(seg.charAt(i));
if (v != lastV) {
alt++;
lastV = v;
}
}
return alt >= seg.length() * 0.4;
}
private static boolean isVowel(char c) {
return “aeiouAEIOU”.indexOf(c) >= 0;
}
}Combine Everything in a Service
Create src/main/java/com/example/service/PasswordStrengthService.java:
package com.example.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.example.analysis.PhoneticMatcher;
import com.example.analysis.SegmentAnalyzer;
import com.example.analysis.SyllableAnalyzer;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PasswordStrengthService {
private final PhoneticMatcher matcher = new PhoneticMatcher();
public Map<String, Object> evaluate(String password) {
// Track if password was originally null to return null in response
boolean wasNull = (password == null);
// Handle null password - treat as empty string for evaluation
String passwordForEvaluation = (password == null) ? “” : password;
int score = 100;
List<String> warnings = new ArrayList<>();
boolean hasPhoneticMatch = false;
// Traditional checks
if (passwordForEvaluation.length() < 8) {
score -= 20;
warnings.add(”Too short (min 8 chars)”);
}
// Syllable density
double density = SyllableAnalyzer.getSyllableDensity(passwordForEvaluation);
if (density > 0.5) {
score -= 20;
warnings.add(”Too easy to pronounce”);
} else if (density < 0.2) {
score += 5;
}
// Phonetic similarity
var result = matcher.check(passwordForEvaluation);
if (result.match()) {
score -= 40;
hasPhoneticMatch = true;
warnings.add(”Sounds like common word: “ + result.pattern());
}
// Segment pronounceability
double pron = SegmentAnalyzer.pronounceability(passwordForEvaluation);
if (pron > 0.7) {
score -= 15;
warnings.add(”Highly pronounceable segments”);
}
// Ensure score is between 0 and 100
score = Math.max(0, Math.min(100, score));
// Return null for password field if it was originally null
// Use HashMap to allow null values (Map.of doesn’t allow null)
Map<String, Object> response = new HashMap<>();
response.put(”password”, wasNull ? null : password);
response.put(”score”, score);
response.put(”warnings”, warnings);
response.put(”rating”, rating(score, hasPhoneticMatch));
return response;
}
private String rating(int score, boolean hasPhoneticMatch) {
// Phonetic similarity to common words is a serious security issue
// Cap the rating at “GOOD” even if score would be higher
if (hasPhoneticMatch && score >= 60) {
return “GOOD”;
}
if (hasPhoneticMatch && score >= 40) {
return “FAIR”;
}
if (score >= 80)
return “STRONG”;
if (score >= 60)
return “GOOD”;
if (score >= 40)
return “FAIR”;
if (score >= 20)
return “WEAK”;
return “VERY_WEAK”;
}
}Scoring Logic
Starting point: Each password starts at 100 points; deductions reduce the score.
Length check: Passwords shorter than 8 characters lose 20 points.
Syllable density:
Density > 0.5 (too easy to pronounce): -20 points
Density < 0.2 (hard to pronounce): +5 points
Phonetic similarity: If the password sounds like a common word (e.g., “password”, “admin”), it loses 40 points and cannot be rated “STRONG” (capped at “GOOD” or “FAIR”).
Segment pronounceability: If segments are highly pronounceable (> 0.7), lose 15 points.
Rating assignment: Based on final score (STRONG ≥80, GOOD ≥60, FAIR ≥40, WEAK ≥20, VERY_WEAK <20), with phonetic matches capped at “GOOD” even if the score would be higher.
Score floor: Final score cannot go below 0.
Expose a REST API
Rename src/main/java/com/example/GreetingResource.java to PasswordStrengthResource.java and replace with the following:
package com.example;
import java.util.Map;
import com.example.service.PasswordStrengthService;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path(”/api/password”)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PasswordStrengthResource {
@Inject
PasswordStrengthService service;
@POST
@Path(”/evaluate”)
public Map<String, Object> evaluate(Map<String, String> body) {
String password = body.get(”password”);
return service.evaluate(password);
}
}Run and Test
Start the application:
quarkus devUse curl or HTTPie to test the endpoint:
curl -X POST http://localhost:8080/api/password/evaluate \
-H "Content-Type: application/json" \
-d '{"password":"P@ssw0rd"}'Expected output:
{
“score”: 65,
“warnings”: [
“Sounds like common word: password”
],
“rating”: “GOOD”,
“password”: “P@ssw0rd”
}Try a harder one:
curl -X POST http://localhost:8080/api/password/evaluate \
-H "Content-Type: application/json" \
-d '{"password":"xK9#mQ2$pL7"}'{
“score”: 100,
“warnings”: [],
“rating”: “STRONG”,
“password”: “xK9#mQ2$pL7”
}And as a regular reader of my blog, it will surprise you, that I did also add a couple of tests to the repository this time :) Go, check them out! ;-)
Production Notes
Security: Do not log passwords. Add rate limiting and IP throttling.
Performance: Cache phonetic encodings for repeated evaluations.
Localization: Use
ColognePhoneticorCaverphone2for regional support.Integration: This can be embedded into registration forms or password policy checks.
More Ideas to try if you want to push it
Real-Time Feedback
Add an endpoint that returns incremental strength feedback:
@GET
@Path("/feedback")
public Map<String, String> feedback(@QueryParam("partial") String partial) {
double d = com.example.analysis.SyllableAnalyzer.getSyllableDensity(partial);
String msg = d > 0.5 ? "Too easy to say – add symbols!" :
d < 0.2 ? "Looking good!" : "Keep going...";
return Map.of("feedback", msg);
}Anti-Pronounceable Password Generator
@GET
@Path("/generate")
public Map<String, String> generate(@QueryParam("len") @DefaultValue("12") int len) {
return Map.of("password", com.example.util.PasswordGenerator.generate(len));
}Check out the PasswordGenerator.java in my Github repository.
Wrap-Up
You’ve just built a phonetic password strength meter with Quarkus. A system that goes beyond entropy and detects how sayable a password is.
By analyzing syllable patterns, phonetic similarity, and pronounceable segments, you can expose a subtle but real security weakness most meters ignore.
The best password isn’t the longest one.
It’s the one you can’t say out loud.





Nice, though, based on a kinda false premise. Passwords made of 5 common words still have a better entropy than most generated passwords, and yet are easy to remember.