前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >我用Java代码模拟出了德国二战的Enigma密码机加密

我用Java代码模拟出了德国二战的Enigma密码机加密

原创
作者头像
半月无霜
修改2024-11-20 16:47:57
修改2024-11-20 16:47:57
40200
代码可运行
举报
文章被收录于专栏:半月无霜半月无霜
运行总次数:0
代码可运行

今日推荐文章:API调用中的身份验证与授权实践-腾讯云开发者社区-腾讯云

点评:深入探讨了API调用的身份认证与授权的问题,并给出了实现,非常值得学习

一、介绍

在二战期间,加密大大添加破解的难度。其中最有名的就属于二战德国的Enigma密码机,号称永远不可能被破解(后面还是被破解了,笑)

恩尼格玛密码机Enigma是二战期间德国广泛使用的加密设备,其复杂的机械结构和加密算法在当时被认为是极其安全的。

下面将详细解释这款密码机是如何对字母进行加密的

二、Enigma密码机加密原因

首先,我们先了解一下电路,其中包含了三样东西,电池、开关、灯;如下图

image-20241115134725566
image-20241115134725566

好的,当开关A按下,电路闭合,灯泡A亮起;非常简单的原因

这是一组电路的情况,现在让我们多添加几个电路

image-20241115135224513
image-20241115135224513

这也非常好理解,当开关按下,对应电路的灯泡将亮起;这非常容易理解

接下来,我们尝试重新组合电路,将灯泡与开关打乱,那么将会得到

image-20241115135429002
image-20241115135429002

仅仅就是改变了电路的位置,那么我们不妨按下开关,会发生什么情况

当按下开关A,灯泡B亮了

当按下开关B,灯泡C亮了

当按下开关C,灯泡A亮了

到了这一步,你已经初步理解了Enigma密码机是如何工作的了,我在上面仅仅只有3个开关,那么只需要将这种电路添加至26个,覆盖到每一个字母,那么就能实现加密了。

新的问题出现了,如果仅仅只是这么简单的电路,那还达不到加密吧

确实,如同凯撒密码一般,如果仅仅只是将字母相互之间替换,语言学家会非常轻松地,将分析常用单词的高频字母,还原出原本的明文。

Enigma密码机,神奇的地方在于,他每次按下相同的字母,亮起的灯泡是不同的。这是如何实现的?

我们引入一个新的东西,转子

转子是一个扁圆柱体,两边各装有26个金属触点,每个金属触点代表了一个字母,如下图

image-20241115141458016
image-20241115141458016

在转子的内部,实现了我们上面给出的简单电路,打乱了它

当按下开关A,转子内部走线会到达B

当按下开关B,转子内部走线会到达C

当按下开关C,转子内部走线会到达另外一个字母

内部就是将26个字母的电路走线打乱,这也非常好理解。

但仅仅只有这个,还不足以完成加密吧

我们继续往下看,一个转子不行,Enigma密码机有三个这样的转子

image-20241115142024834
image-20241115142024834

转子相互组合,触点两两相连,假设

我们按下开关A,

电流经过右边转子后,输出成为了B

接着电流进过中间转子后,输出成为了G

再接着经过左边转子后,输出成为了Z

好的,以为就这样结束了吗?不,电流会经过反射器还会回来

image-20241115143937718
image-20241115143937718

那么也就是说,前面左边转子输入了个Z进入反射器,

反射器会经过内部两两相连的布线,返回个Y给到左边转子,

经过左边转子内部过后,返回个C给到中间转子

经过中间转子内部过后,返回个E给到右边转子

至此联通电路,可以计算到,当中经过了7次映射,可以算作7次凯撒密码的加密

还没完呢,这有三个转子A、B、C,分别可以进行组合,这有多少种可能

  1. A、B、C
  2. A、C、B
  3. B、A、C
  4. B、C、A
  5. C、A、B
  6. C、B、A

不急,还不止这些,在Enigma机器按下开关的那一刻,会使得转子转动

按照上面排列的顺序不同,我们区分一下,分别是低速转子、中速转子、高速转子

就像时针、分针、秒针一样。秒针转一圈,分针跳一刻度;分针转一圈,时针跳一刻度

当然,Enigma机器转子的一圈是26,也就是英文字母的数字,26进制可以理解吗?

正是因为转子的转动,才会使得我们,就算连续按下相同的字母,亮起的字母灯也是不一样的

这也就是说,只要确认了,转子的初始摆放位置,我们就能对密文进行解密

假设我们的转子是这样摆放的,A、B、C,且每个转子的初始刻度都为1

我们对一段明文加密后,将密文给到德军下级

德军下级,将Enigma机器的转子位置,转子的初始刻度摆放得和我们一样

它将密文一个一个字母输入后,一个一个灯泡亮起,就得到了解密后的明文

忘记说一个概念了 除了对三个转子进行组合外,每个转子都有自己的状态,即转子当前的初始摆放刻度 在转子的环面上,刻上了1-26的数字,就是代表转子刻度 哪个数字显示,就代表了当前转子所处的刻度

这也就是Enigma密码机,神奇的地方,加密机即为解密机

最后一个保险,就是接插板,这比起转子,原理非常简单。

就是在机器前面,添加了扰乱映射,通过电线将字母两两相连

由于机器大小原因,这种两两字母之间的映射,只能够添加10

当我将字母A-Z相连,键盘按下A、将以Z的电路往后走去


好了,让我们回顾一下整个电路的流程

  1. 当按下键盘的字母后,快速转子转动(这是机械结构,不需要电路。看情况会不会带动后面的转子)
  2. 电流会先经过接插板(就看你的混淆连线情况了,有混淆的会以混淆的字母继续往后走)
  3. 然后正向经过转子组,右边转子、中间转子、左边转子
  4. 经过反射器,再然后反向经过转子组,左边转子、中间转子、右边转子
  5. 电流再次经过接插板(再看一遍混淆连线情况,有混淆的会以混淆的字母继续往后走)
  6. 这时候连接到对应的灯泡了,电流再经过电池
  7. 电路闭合,对应的灯泡亮起

三、Java代码还原

1)转子组

我们需要转子类、反射器类、转子组类;

先编写简单的反射器类,顺便把加密的方法写了。很简单就是从一个字母,映射成为另一个字母

反射器的构造器,就是为了维护这个字母映射关系

代码语言:javascript
代码运行次数:0
复制
 package com.banmoon.enigma.optimize;
 ​
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 import lombok.Data;
 ​
 import java.util.HashMap;
 import java.util.Map;
 ​
 @Data
 public final class Reflector {
 ​
     public static final Reflector INSTANT = new Reflector("ZYXWVUTSRQPONMLKJIHGFEDCBA");
 ​
     private final Map<Character, Character> mapping;
 ​
     /**
      * 构造器
      *
      * @param encodingStr 编码,用于构造反射器
      */
     private Reflector(String encodingStr) {
         Assert.isTrue(StrUtil.isNotBlank(encodingStr), "encodingStr cannot be blank");
         Assert.isTrue(StrUtil.length(encodingStr) == Constant.SIZE, "encodingStr length must be 26");
         this.mapping = new HashMap<>(Constant.SIZE);
         char[] charArray = encodingStr.toCharArray();
         for (int i = 0; i < charArray.length; i++) {
             char c = (char) (i + Constant.A_CHAR);
             this.mapping.put(c, charArray[i]);
         }
     }
 ​
     /**
      * 加密
      *
      * @param c 字符
      * @return 加密字符
      */
     public char encrypt(char c) {
         return this.mapping.get(c);
     }
 ​
 }

那么接下来,就是转子,与转子组

转子类,我就写了个静态内部类,放在转子组中

转子组类中,有一个转子数组,用来维护转子

rotors[0],慢速转子,最左边的转子

rotors[1],中间转子,中间的转子

rotors[2],快速转子,最右边的转子

同时一个转动方法rotate(),从最右边的转子转动,判断是否经过了一圈,从而带动下一个转子转动

在转子内部维护了两个映射数组,分别是正向表与反向表

假设当前转子的当前定位currentPosition是处于0,当我们正向输入A时,我们就取正向表0索引的字符输出

假设当前转子的当前定位currentPosition是处于0,当我们反向输入A时,我们就取反向表0索引的字符输出

当然啦,currentPosition不可能一直是0,输入的字母也不能一直是A,这时候我们就要计算它的偏移量,找到正确的触点进行输出

代码语言:javascript
代码运行次数:0
复制
 package com.banmoon.enigma.optimize;
 ​
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.StrUtil;
 import lombok.Data;
 ​
 /**
  * 转子组
  */
 @Data
 public class RotorSet {
 ​
     /**
      * 提供了三个转子,用来组合
      */
     public static final Rotor ROTOR_A = new Rotor("A", "COMPUTERSVWXYZABDFGHIJKLNQ");
     public static final Rotor ROTOR_B = new Rotor("B", "HIJKLNQCOMPUTERSVWXYZABDFG");
     public static final Rotor ROTOR_C = new Rotor("C", "PUTEABDFGRSVIMWXYJHKLNQCOZ");
 ​
     /**
      * 3个转子组成 <br>
      * rotors[0]:低速转子,中速转子转动26下,此转子转动一次
      * rotors[1]:中速转子,高速转子转动26下,此转子转动一次
      * rotors[2]:高速转子,每键入一次字符,此转子转动一次
      */
     private final Rotor[] rotors;
 ​
     /**
      * 反射器
      */
     private final Reflector reflector;
 ​
     /**
      * 当前转子处于哪个定位,通过定位可以确定转子上的字符
      */
     private final int[] currentPositionArray;
 ​
     /**
      * 构造转子组<br>
      * 设置转子的顺序,3个转子如何摆放
      *
      * @param leftRotor   左边的转子,低速转子
      * @param middleRotor 中间的转子,中速转子
      * @param rightRotor  右边的转子,高速转子
      * @param reflector   反射器
      * @param initPositionStr 初始字符,用于设置转子位置
      */
     public RotorSet(Rotor leftRotor, Rotor middleRotor, Rotor rightRotor, Reflector reflector, String initPositionStr) {
         this(leftRotor, middleRotor, rightRotor, reflector, StrUtil.split(initPositionStr, StrUtil.COMMA).stream().map(Convert::toInt).toArray(Integer[]::new));
     }
 ​
     /**
      * 构造转子组<br>
      * 设置转子的顺序,3个转子如何摆放
      *
      * @param leftRotor     左边的转子,低速转子
      * @param middleRotor   中间的转子,中速转子
      * @param rightRotor    右边的转子,高速转子
      * @param reflector     反射器
      * @param initPositionArray 初始位置,用于设置转子位置
      */
     public RotorSet(Rotor leftRotor, Rotor middleRotor, Rotor rightRotor, Reflector reflector, Integer[] initPositionArray) {
         this.rotors = new Rotor[]{leftRotor, middleRotor, rightRotor};
         this.reflector = reflector;
         if (ArrayUtil.isNotEmpty(initPositionArray)) {
             Assert.isTrue(initPositionArray.length == 3, "initPositionArray length must be 3");
             leftRotor.setInitPosition(initPositionArray[0]);
             middleRotor.setInitPosition(initPositionArray[1]);
             rightRotor.setInitPosition(initPositionArray[2]);
             this.currentPositionArray = new int[]{initPositionArray[0], initPositionArray[1], initPositionArray[2]};
         } else {
             this.currentPositionArray = new int[]{0, 0, 0};
         }
     }
 ​
     /**
      * 转动转子
      */
     public void rotate() {
         if (rotors[2].rotate()) {
             if (rotors[1].rotate()) {
                 rotors[0].rotate();
             }
         }
         currentPositionArray[0] = rotors[0].getCurrentPosition();
         currentPositionArray[1] = rotors[1].getCurrentPosition();
         currentPositionArray[2] = rotors[2].getCurrentPosition();
     }
 ​
     /**
      * 加密
      *
      * @param c 字符
      * @return 加密字符
      */
     public char reflectorEncrypt(char c) {
         return reflector.encrypt(c);
     }
 ​
     /**
      * 转子
      */
     @Data
     public static class Rotor {
 ​
         /**
          * 转子名称
          */
         private String name;
 ​
         /**
          * 26位编码表
          */
         private int[] encoding;
 ​
         /**
          * 26位编码表,反向
          */
         private int[] inverseEncoding;
 ​
         /**
          * 初始转子的定位
          */
         private int initPosition;
 ​
         /**
          * 当前转子的定位
          */
         private int currentPosition;
 ​
         public Rotor(String name, String encodingStr) {
             Assert.isTrue(StrUtil.isNotBlank(encodingStr), "encodingStr cannot be blank");
             Assert.isTrue(encodingStr.length() == Constant.SIZE, "encodingStr length must be 26");
             char[] charArray = encodingStr.toCharArray();
             this.name = name;
             this.initPosition = 0;
             this.currentPosition = 0;
             this.encoding = new int[charArray.length];
             this.inverseEncoding = new int[charArray.length];
             for (int i = 0; i < charArray.length; i++) {
                 this.encoding[i] = charArray[i] - Constant.A_CHAR;
             }
             for (int i = 0; i < this.encoding.length; i++) {
                 int index = this.encoding[i];
                 this.inverseEncoding[index] = i;
             }
         }
 ​
         public void setInitPosition(int initPosition) {
             this.initPosition = initPosition;
             this.currentPosition = initPosition;
         }
 ​
         /**
          * 转动转子
          *
          * @return 是否完成转子一圈的转动
          */
         public boolean rotate() {
             currentPosition = (currentPosition + 1) % Constant.SIZE;
             return currentPosition == initPosition;
         }
 ​
     }
 ​
 }

2)插接板

插接板的代码比较简单

维护了一个字母映射的表,正常构造器中是输入什么就输出什么

混淆了字母,就会两两相连

代码语言:javascript
代码运行次数:0
复制
 package com.banmoon.enigma.optimize;
 ​
 import cn.hutool.core.lang.Assert;
 import lombok.Data;
 ​
 import java.util.HashMap;
 import java.util.Map;
 ​
 /**
  * 插接板
  */
 @Data
 public class Plugboard {
 ​
     private final Map<Character, Character> mapping;
 ​
     public Plugboard() {
         this.mapping = new HashMap<>(Constant.SIZE);
         for (char c = 'A'; c <= 'Z'; c++) {
             this.mapping.put(c, c);
         }
     }
 ​
     /**
      * 混淆
      */
     public void confuse(Character key, Character value) {
         Assert.isTrue(key >= 'A' && key <= 'Z', "plugboard key must be a letter");
         Assert.isTrue(value >= 'A' && value <= 'Z', "plugboard value must be a letter");
         mapping.put(key, value);
         mapping.put(value, key);
     }
 ​
     /**
      * 混淆
      */
     public void confuse(Map<Character, Character> mapping) {
         mapping.forEach(this::confuse);
     }
 ​
     /**
      * 加密
      *
      * @param c 字符
      * @return 加密字符
      */
     public char encrypt(char c) {
         return mapping.get(c);
     }
 }

3)Enigma.java

接下来就是组合代码了,也是比较简单,就是将转子组与接插板组合在一起的类

代码语言:javascript
代码运行次数:0
复制
 package com.banmoon.enigma.optimize;
 ​
 import cn.hutool.core.lang.Assert;
 import lombok.Data;
 ​
 import java.util.Map;
 ​
 /**
  * 恩格码密码机
  */
 @Data
 public class EnigmaMachine {
 ​
     /**
      * 转子组
      */
     private RotorSet rotorSet;
 ​
     /**
      * 插接板
      */
     private Plugboard plugboard;
 ​
     public EnigmaMachine(RotorSet rotorSet, Plugboard plugboard) {
         this.rotorSet = rotorSet;
         this.plugboard = plugboard;
     }
 ​
     /**
      * 混淆插接板
      */
     public void confusePlugboard(Character key, Character value) {
         plugboard.confuse(key, value);
     }
 ​
     /**
      * 混淆插接板
      */
     public void confusePlugboard(Map<Character, Character> mapping) {
         plugboard.confuse(mapping);
     }
 ​
 }

4)加密方法代码

重头戏在于,加密方法的编写,我们从EnigmaMachine.java开始

给予两个加密重载方法encrypt()

注意看形参为char的方法,它的步骤就是我们上面分析的电路连通步骤

代码语言:javascript
代码运行次数:0
复制
     /**
      * 加密
      *
      * @param input 字符串
      * @return 加密字符串
      */
     public String encrypt(String input) {
         Assert.notBlank(input, "input can not be blank");
         input = input.toUpperCase();
         char[] charArray = input.toCharArray();
         char[] result = new char[charArray.length];
         for (int i = 0; i < charArray.length; i++) {
             char c = charArray[i];
             if (c >= 'A' && c <= 'Z') {
                 c = encrypt(c);
             }
             result[i] = c;
         }
         return new String(result);
     }
 ​
     /**
      * 加密
      *
      * @param c 字符
      * @return 加密字符
      */
     public char encrypt(char c) {
         // 每输入一次字符,都会使转子旋转
         rotorSet.rotate();
         // 经过插接板
         c = plugboard.encrypt(c);
         // 经过转子组的转子
         c = rotorSet.encrypt(c);
         // 经过转子组的反射器
         c = rotorSet.reflectorEncrypt(c);
         // 再次经过转子组的转子(反向)
         c = rotorSet.reverseEncrypt(c);
         // 再次经过插接板
         return plugboard.encrypt(c);
     }

最麻烦的就是转子组当中的加密,调试这个偏移量,可是费了不少脑细胞

首先是转子组的正向加密,可以看到是从慢速转子进入,经过中速转子,再经过慢速转子

代码语言:javascript
代码运行次数:0
复制
     /**
      * 加密
      *
      * @param c 字符
      * @return 加密字符
      */
     public char encrypt(char c) {
         int i = c - Constant.A_CHAR;
         int i1 = rotors[2].encrypt(i);
         int i2 = rotors[1].encrypt(i1);
         int i3 = rotors[0].encrypt(i2);
         return (char) (i3 + Constant.A_CHAR);
     }

那么在转子中,这个加密方法是什么样的呢

首先计算出了当前转子偏移量,

再计算出当前转子输入字符的位置是哪个触点

获取到当前触点,经过映射之后的字符

再计算出映射之后字符的触点位置

将这个信息返回出去,回到转子组可以看到,带着这个触点位置的信息,输入进了三个转子

代码语言:javascript
代码运行次数:0
复制
     /**
      * 加密
      *
      * @param charIndex 字符所在位置
      * @return 加密字符所在位置
      */
     public int encrypt(int charIndex) {
         int offset = currentPosition - initPosition;
         int index = (charIndex + offset + 26) % 26;
         return (encoding[index] - offset + 26) % 26;
     }

那么同理,经过反射器之后,返回的情况也是一样的

我们先看转子组的

代码语言:javascript
代码运行次数:0
复制
     /**
      * 反向加密
      *
      * @param c 字符
      * @return 加密字符
      */
     public char reverseEncrypt(char c) {
         int i = c - Constant.A_CHAR;
         int i1 = rotors[0].reverseEncrypt(i);
         int i2 = rotors[1].reverseEncrypt(i1);
         int i3 = rotors[2].reverseEncrypt(i2);
         return (char) (i3 + Constant.A_CHAR);
     }

再看转子的

代码语言:javascript
代码运行次数:0
复制
     /**
      * 加密
      *
      * @param charIndex 字符所在位置
      * @return 加密字符所在位置
      */
     public int reverseEncrypt(int charIndex) {
         int offset = currentPosition - initPosition;
         int index = (charIndex + offset + 26) % 26;
         return (inverseEncoding[index] - offset + 26) % 26;
     }

这边代码有点难懂,简单的来说,就是给每个触点编上了号码,0-25,当你输入A,对应触点的编号就是0

但当前编号为0的触点,连接的转子触点不是0啊,这个需要看偏移量,才能确定上当前与0号触点连接的转子触点是几号

确认完转子几号触点之后,还要看这个触点映射为哪个字符,

再通过偏移量,确认这个字符在当前转子的哪个触点输出

5)测试

我们来进行测试

代码语言:javascript
代码运行次数:0
复制
 package com.banmoon.enigma.optimize;
 ​
 import static com.banmoon.enigma.optimize.RotorSet.*;
 ​
 public class Main {
 ​
     public static void main(String[] args) {
         RotorSet rotorSet = new RotorSet(ROTOR_A, ROTOR_B, ROTOR_C, Reflector.INSTANT, "0,0,0");
         Plugboard plugboard = new Plugboard();
         plugboard.confuse('A', 'B');
         plugboard.confuse('C', 'D');
         plugboard.confuse('E', 'F');
         EnigmaMachine enigmaMachine = new EnigmaMachine(rotorSet, plugboard);
 ​
 ​
         String a = enigmaMachine.encrypt("my name is banmoon");
         System.out.println(a);
 ​
         // 再试另一个密码机解密
         RotorSet rotorSet1 = new RotorSet(ROTOR_A, ROTOR_B, ROTOR_C, Reflector.INSTANT, "0,0,0");
         EnigmaMachine enigmaMachine1 = new EnigmaMachine(rotorSet1, plugboard);
         a = enigmaMachine1.encrypt("FU PHZX YX PJYIVBF");
         System.out.println(a);
     }
 ​
 }
image-20241115161926343
image-20241115161926343

就算盟军能拿到Enigma密码机,但由于不知道正确的转子摆放位置,以及插接板的混淆情况

破解密码几乎是不肯能的,我们来试一下,如果转子位置不同,设置的插接板情况不一样会怎么样

代码语言:javascript
代码运行次数:0
复制
 package com.banmoon.enigma.optimize;
 ​
 import static com.banmoon.enigma.optimize.RotorSet.*;
 ​
 public class Main {
 ​
     public static void main(String[] args) {
         RotorSet rotorSet = new RotorSet(ROTOR_A, ROTOR_B, ROTOR_C, Reflector.INSTANT, "0,0,0");
         Plugboard plugboard = new Plugboard();
         plugboard.confuse('A', 'B');
         plugboard.confuse('C', 'D');
         plugboard.confuse('E', 'F');
         EnigmaMachine enigmaMachine = new EnigmaMachine(rotorSet, plugboard);
 ​
 ​
         String a = enigmaMachine.encrypt("my name is banmoon");
         System.out.println(a);
 ​
         // 再试另一个密码机解密
         RotorSet rotorSet1 = new RotorSet(ROTOR_A, ROTOR_C, ROTOR_B, Reflector.INSTANT, "12,11,10");
         EnigmaMachine enigmaMachine1 = new EnigmaMachine(rotorSet1, plugboard);
         a = enigmaMachine1.encrypt("FU PHZX YX PJYIVBF");
         System.out.println(a);
     }
 ​
 }
image-20241115162243528
image-20241115162243528

四、最后

不得不说,图灵不愧为计算机之父,这么复杂的密码机,都能破解

以后可以讲解一下,图灵祖师爷是如何破解这台Enigma密码机的

附带一张图片,了解一下这么个加密机的构造,更好了解它的加密原理

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 二、Enigma密码机加密原因
  • 三、Java代码还原
    • 1)转子组
    • 2)插接板
    • 3)Enigma.java
    • 4)加密方法代码
    • 5)测试
  • 四、最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档