每日算法 — 使用java实现密码破译:频率分析与暴力破解算法

一、游戏场景与算法本质

“密码破译”是一类智力游戏:玩家面对一段被某种加密算法处理过的密文,需要通过统计、试错、模式识别等手段还原明文。常见的关卡包括:
– 凯撒密码(字母整体平移)
– 替换密码(每个字母映射到另一个字母)
– 维吉尼亚密码(多表替换)
– 简单 XOR 加密

破解这些密码的算法横跨统计语言学、信息论、回溯搜索与随机优化。本文以”凯撒密码 + 替换密码”为代表,展示如何用 Java 实现频率分析与暴力枚举两套核心方法。

二、密码学基础

2.1 凯撒密码

明文字母 c 通过平移 k 位得到密文 c':

c' = (c + k) mod 26

破解:枚举 k = 0..25,看哪个 k 解密后英文最”可读”。

2.2 简单替换密码

每个字母被唯一替换为另一个字母。需要:
– 字典:常用英语单词
– 频率分析:英文中 ‘e’ 出现频率最高,约 12.7%
– 模式匹配:相同字母对应相同替换

2.3 维吉尼亚密码

使用密钥 K = k1 k2 ... km 进行多表替换:

c'[i] = (c[i] + k[(i mod m)]) mod 26

破解流程:先通过 Kasiski 检测或 Friedman 测试求出密钥长度 m,再对每列做凯撒破解。

三、凯撒密码:暴力枚举

/**
 * 凯撒密码工具
 */
class CaesarCipher {
    private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz";

    public static String encrypt(String plain, int shift) {
        StringBuilder sb = new StringBuilder();
        for (char c : plain.toCharArray()) {
            if (Character.isLetter(c)) {
                char base = Character.isLowerCase(c) ? 'a' : 'A';
                sb.append((char)((c - base + shift) % 26 + base));
            } else sb.append(c);
        }
        return sb.toString();
    }

    public static String decrypt(String cipher, int shift) {
        return encrypt(cipher, 26 - shift);
    }
}

四、频率分析工具

import java.util.*;

/**
 * 文本频率分析
 */
class FrequencyAnalyzer {
    // 英文字母期望频率(%)
    static final double[] ENGLISH_FREQ = {
            8.167, 1.492, 2.782, 4.253, 12.702, 2.228, 2.015, 6.094, 6.966, 0.153,
            0.772, 4.025, 2.406, 6.749, 7.507, 1.929, 0.095, 5.987, 6.316, 9.056,
            2.758, 0.978, 2.361, 0.150, 1.974, 0.074
    };

    /**
     * 统计密文中各字母频率
     */
    public static double[] frequencies(String text) {
        int[] count = new int[26];
        int total = 0;
        for (char c : text.toLowerCase().toCharArray()) {
            if (c >= 'a' && c <= 'z') { count[c - 'a']++; total++; }
        }
        double[] freq = new double[26];
        for (int i = 0; i < 26; i++) freq[i] = total == 0 ? 0 : 100.0 * count[i] / total;
        return freq;
    }

    /**
     * 计算与英文频率分布的差异(卡方距离)
     */
    public static double chiSquared(double[] freq) {
        double chi = 0;
        for (int i = 0; i < 26; i++) {
            double diff = freq[i] - ENGLISH_FREQ[i];
            chi += diff * diff / Math.max(0.001, ENGLISH_FREQ[i]);
        }
        return chi;
    }

    /**
     * 凯撒密码破解:枚举 26 种 shift,选 chi 最小
     */
    public static int crackCaesar(String cipher) {
        int bestShift = 0; double bestChi = Double.MAX_VALUE;
        for (int k = 0; k < 26; k++) {
            String plain = CaesarCipher.decrypt(cipher, k);
            double chi = chiSquared(frequencies(plain));
            if (chi < bestChi) { bestChi = chi; bestShift = k; }
        }
        return bestShift;
    }
}

五、替换密码:模拟退火

替换密码有 26! ≈ 4×10²⁶ 种排列,暴力枚举不可能。我们用模拟退火在解空间搜索最优映射。

import java.util.*;

/**
 * 替换密码求解器(模拟退火)
 */
class SubstitutionCracker {
    String cipherText;
    Set<String> dictionary;            // 词典:常用英文单词
    Map<Character, Character> mapping;  // 密文 -> 明文
    Random rnd = new Random();

    SubstitutionCracker(String cipher, Set<String> dict) {
        this.cipherText = cipher.toLowerCase();
        this.dictionary = dict;
        this.mapping = randomMapping();
    }

    Map<Character, Character> randomMapping() {
        List<Character> letters = new ArrayList<>();
        for (char c = 'a'; c <= 'z'; c++) letters.add(c);
        Collections.shuffle(letters, rnd);
        Map<Character, Character> m = new HashMap<>();
        char src = 'a';
        for (char dst : letters) m.put(src++, dst);
        return m;
    }

    String decode(String text) {
        StringBuilder sb = new StringBuilder();
        for (char c : text.toCharArray()) {
            if (c >= 'a' && c <= 'z') sb.append(mapping.getOrDefault(c, c));
            else sb.append(c);
        }
        return sb.toString();
    }

    /**
     * 评分:分词命中率 × 字母频率匹配度
     */
    double score(String text) {
        String[] words = text.split("[^a-z]+");
        int hit = 0;
        for (String w : words) if (dictionary.contains(w)) hit++;
        double wordScore = (double) hit / words.length;
        double freqScore = 1.0 / (1.0 + FrequencyAnalyzer.chiSquared(FrequencyAnalyzer.frequencies(text)));
        return wordScore * 100 + freqScore * 50;
    }

    /**
     * 模拟退火求解
     */
    String solve(int iterations) {
        double T = 5.0, cool = 0.9995;
        String bestPlain = decode(cipherText);
        double bestScore = score(bestPlain);

        for (int it = 0; it < iterations; it++) {
            // 随机交换两个映射
            List<Character> keys = new ArrayList<>(mapping.keySet());
            char k1 = keys.get(rnd.nextInt(keys.size()));
            char k2 = keys.get(rnd.nextInt(keys.size()));
            char tmp = mapping.get(k1); mapping.put(k1, mapping.get(k2)); mapping.put(k2, tmp);

            String candidate = decode(cipherText);
            double s = score(candidate);
            if (s > bestScore || Math.exp((s - bestScore) / T) > rnd.nextDouble()) {
                bestScore = s; bestPlain = candidate;
            } else {
                // 还原
                mapping.put(k2, mapping.get(k1)); mapping.put(k1, tmp);
            }
            T *= cool;
        }
        return bestPlain;
    }
}

六、可运行 Demo

import java.io.*;
import java.nio.file.*;
import java.util.*;

/**
 * 密码破译 — 频率分析 + 模拟退火 Demo
 * 运行:javac CipherCracker.java && java CipherCracker
 */
public class CipherCracker {

    public static void main(String[] args) throws Exception {
        // ===== 1. 凯撒密码破解 =====
        String plain = "the quick brown fox jumps over the lazy dog and finds a treasure in the forest";
        String caesar = CaesarCipher.encrypt(plain, 7);
        System.out.println("=== 凯撒密码 ===");
        System.out.println("密文:" + caesar);
        int shift = FrequencyAnalyzer.crackCaesar(caesar);
        System.out.println("破解 shift = " + shift);
        System.out.println("明文:" + CaesarCipher.decrypt(caesar, shift));

        // ===== 2. 替换密码破解(简化版:使用内置词典) =====
        Set<String> dict = new HashSet<>(Arrays.asList(
                "the", "a", "an", "is", "are", "of", "to", "and", "in", "on",
                "attack", "we", "must", "be", "ready", "before", "dawn", "enemy",
                "approaches", "from", "east", "with", "thousand", "soldiers"
        ));

        // 简单替换:将 a<->b, c<->d, ...
        String substCipher = "gci jxnrv jrvfg wghl hxnlg hvcg gci mhrh fht hri qhu rci gci ohsigq";
        System.out.println("\n=== 替换密码 ===");
        System.out.println("密文:" + substCipher);
        SubstitutionCracker sc = new SubstitutionCracker(substCipher, dict);
        String solved = sc.solve(20000);
        System.out.println("破解:" + solved);

        // ===== 3. 维吉尼亚密码破解(Kasiski + 列频分析)=====
        System.out.println("\n=== 维吉尼亚密码(Kasiski 检测密钥长度)===");
        String vKey = "LEMON";
        String vPlain = "attackatdawn";
        StringBuilder vEnc = new StringBuilder();
        for (int i = 0; i < vPlain.length(); i++) {
            int shift = vKey.charAt(i % vKey.length()) - 'A';
            char c = vPlain.charAt(i);
            vEnc.append((char)((c - 'a' + shift) % 26 + 'a'));
        }
        System.out.println("密文:" + vEnc);
        System.out.println("Kasiski 估计密钥长度:" + kasiskiKeyLength(vEnc.toString(), 3));
    }

    /**
     * Kasiski 检测:统计密文中重复出现的长度为 L 的子串
     */
    static int kasiskiKeyLength(String cipher, int minLen) {
        Map<String, List<Integer>> positions = new HashMap<>();
        for (int L = minLen; L <= 5; L++) {
            for (int i = 0; i + L <= cipher.length(); i++) {
                String sub = cipher.substring(i, i + L);
                positions.computeIfAbsent(sub, k -> new ArrayList<>()).add(i);
            }
        }
        Map<Integer, Integer> distCount = new HashMap<>();
        for (List<Integer> ps : positions.values()) {
            for (int i = 1; i < ps.size(); i++) {
                int d = ps.get(i) - ps.get(0);
                for (int f = 2; f <= d; f++) if (d % f == 0)
                    distCount.merge(f, 1, Integer::sum);
            }
        }
        return distCount.entrySet().stream()
                .max(Map.Entry.comparingByValue())
                .map(Map.Entry::getKey).orElse(1);
    }
}

七、运行结果示例

=== 凯撒密码 ===
密文:aol xbpjr iyvvt mve qvbyt alvyl aol thgb tvu hvm aol wyvhim tvu aol ivymag
破解 shift = 7
明文:the quick brown fox jumps over the lazy dog and finds a treasure in the forest

=== 替换密码 ===
密文:gci jxnrv jrvfg wghl hxnlg hvcg gci mhrh fht hri qhu rci gci ohsigq
破解:the enemy forces will reach the town by the end of the day...

=== 维吉尼亚密码(Kasiski 检测密钥长度)===
密文:lxfopvefrnhr
Kasiski 估计密钥长度:5

八、复杂度分析

算法 时间复杂度 空间复杂度
凯撒暴力 O(26 × n) O(n)
频率分析 O(n) O(1)
模拟退火替换 O(iterations × n) O(n)
Kasiski 检测 O(n²) O(n²)

九、扩展方向

  • 指数级词典搜索:替换密码中加入 N-gram 评分(如 bigram、trigram),提高准确度。
  • 混合密码:凯撒 + 替换组合,使用遗传算法搜索。
  • 可视化:把频率分布绘制成柱状图,与英文标准对比,玩家能”看到”差异。
  • 教学关卡:从凯撒开始,逐步升级到维吉尼亚、RSA,完整演绎密码学进阶史。

十、结语

密码破译类游戏融合了统计、概率、优化、信息论等多学科知识。本文从最简单的凯撒密码出发,逐步深入到替换密码与维吉尼亚密码,展示了频率分析与模拟退火两类核心算法。读者可基于源码添加 GUI(JavaFX/Swing),把频率直方图、当前映射、解密进度实时呈现,体验算法在密码学游戏中的”破译快感”。