Skip to content

Commit 45ede4b

Browse files
committed
2 parents 98a95c8 + e91e993 commit 45ede4b

File tree

6 files changed

+413
-0
lines changed

6 files changed

+413
-0
lines changed

src/core/config/Categories.json

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
"DES Decrypt",
8080
"Triple DES Encrypt",
8181
"Triple DES Decrypt",
82+
"LS47 Encrypt",
83+
"LS47 Decrypt",
8284
"RC2 Encrypt",
8385
"RC2 Decrypt",
8486
"RC4",

src/core/lib/LS47.mjs

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/**
2+
* @author n1073645 [[email protected]]
3+
* @copyright Crown Copyright 2020
4+
* @license Apache-2.0
5+
*/
6+
7+
import OperationError from "../errors/OperationError.mjs";
8+
9+
const letters = "_abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()";
10+
const tiles = [];
11+
12+
/**
13+
* Initialises the tiles with values and positions.
14+
*/
15+
export function initTiles() {
16+
for (let i = 0; i < 49; i++)
17+
tiles.push([letters.charAt(i), [Math.floor(i/7), i % 7]]);
18+
}
19+
20+
/**
21+
* Rotates the key "down".
22+
*
23+
* @param {string} key
24+
* @param {number} col
25+
* @param {number} n
26+
* @returns {string}
27+
*/
28+
function rotateDown(key, col, n) {
29+
const lines = [];
30+
for (let i = 0; i < 7; i++)
31+
lines.push(key.slice(i*7, (i + 1) * 7));
32+
const lefts = [];
33+
let mids = [];
34+
const rights = [];
35+
lines.forEach((element) => {
36+
lefts.push(element.slice(0, col));
37+
mids.push(element.charAt(col));
38+
rights.push(element.slice(col+1));
39+
});
40+
n = (7 - n % 7) % 7;
41+
mids = mids.slice(n).concat(mids.slice(0, n));
42+
let result = "";
43+
for (let i = 0; i < 7; i++)
44+
result += lefts[i] + mids[i] + rights[i];
45+
return result;
46+
}
47+
48+
/**
49+
* Rotates the key "right".
50+
*
51+
* @param {string} key
52+
* @param {number} row
53+
* @param {number} n
54+
* @returns {string}
55+
*/
56+
function rotateRight(key, row, n) {
57+
const mid = key.slice(row * 7, (row + 1) * 7);
58+
n = (7 - n % 7) % 7;
59+
return key.slice(0, 7 * row) + mid.slice(n) + mid.slice(0, n) + key.slice(7 * (row + 1));
60+
}
61+
62+
/**
63+
* Finds the position of a letter in the tiles.
64+
*
65+
* @param {string} letter
66+
* @returns {string}
67+
*/
68+
function findIx(letter) {
69+
for (let i = 0; i < tiles.length; i++)
70+
if (tiles[i][0] === letter)
71+
return tiles[i][1];
72+
throw new OperationError("Letter " + letter + " is not included in LS47");
73+
}
74+
75+
/**
76+
* Derives key from the input password.
77+
*
78+
* @param {string} password
79+
* @returns {string}
80+
*/
81+
export function deriveKey(password) {
82+
let i = 0;
83+
let k = letters;
84+
for (const c of password) {
85+
const [row, col] = findIx(c);
86+
k = rotateDown(rotateRight(k, i, col), i, row);
87+
i = (i + 1) % 7;
88+
}
89+
return k;
90+
}
91+
92+
/**
93+
* Checks the key is a valid key.
94+
*
95+
* @param {string} key
96+
*/
97+
function checkKey(key) {
98+
if (key.length !== letters.length)
99+
throw new OperationError("Wrong key size");
100+
const counts = new Array();
101+
for (let i = 0; i < letters.length; i++)
102+
counts[letters.charAt(i)] = 0;
103+
for (const elem of letters) {
104+
if (letters.indexOf(elem) === -1)
105+
throw new OperationError("Letter " + elem + " not in LS47!");
106+
counts[elem]++;
107+
if (counts[elem] > 1)
108+
throw new OperationError("Letter duplicated in the key!");
109+
}
110+
}
111+
112+
/**
113+
* Finds the position of a letter in they key.
114+
*
115+
* @param {letter} key
116+
* @param {string} letter
117+
* @returns {object}
118+
*/
119+
function findPos (key, letter) {
120+
const index = key.indexOf(letter);
121+
if (index >= 0 && index < 49)
122+
return [Math.floor(index/7), index%7];
123+
throw new OperationError("Letter " + letter + " is not in the key!");
124+
}
125+
126+
/**
127+
* Returns the character at the position on the tiles.
128+
*
129+
* @param {string} key
130+
* @param {object} coord
131+
* @returns {string}
132+
*/
133+
function findAtPos(key, coord) {
134+
return key.charAt(coord[1] + (coord[0] * 7));
135+
}
136+
137+
/**
138+
* Returns new position by adding two positions.
139+
*
140+
* @param {object} a
141+
* @param {object} b
142+
* @returns {object}
143+
*/
144+
function addPos(a, b) {
145+
return [(a[0] + b[0]) % 7, (a[1] + b[1]) % 7];
146+
}
147+
148+
/**
149+
* Returns new position by subtracting two positions.
150+
* Note: We have to manually do the remainder division, since JS does not
151+
* operate correctly on negative numbers (e.g. -3 % 4 = -3 when it should be 1).
152+
*
153+
* @param {object} a
154+
* @param {object} b
155+
* @returns {object}
156+
*/
157+
function subPos(a, b) {
158+
const asub = a[0] - b[0];
159+
const bsub = a[1] - b[1];
160+
return [asub - (Math.floor(asub/7) * 7), bsub - (Math.floor(bsub/7) * 7)];
161+
}
162+
163+
/**
164+
* Encrypts the plaintext string.
165+
*
166+
* @param {string} key
167+
* @param {string} plaintext
168+
* @returns {string}
169+
*/
170+
function encrypt(key, plaintext) {
171+
checkKey(key);
172+
let mp = [0, 0];
173+
let ciphertext = "";
174+
for (const p of plaintext) {
175+
const pp = findPos(key, p);
176+
const mix = findIx(findAtPos(key, mp));
177+
let cp = addPos(pp, mix);
178+
const c = findAtPos(key, cp);
179+
ciphertext += c;
180+
key = rotateRight(key, pp[0], 1);
181+
cp = findPos(key, c);
182+
key = rotateDown(key, cp[1], 1);
183+
mp = addPos(mp, findIx(c));
184+
}
185+
return ciphertext;
186+
}
187+
188+
/**
189+
* Decrypts the ciphertext string.
190+
*
191+
* @param {string} key
192+
* @param {string} ciphertext
193+
* @returns {string}
194+
*/
195+
function decrypt(key, ciphertext) {
196+
checkKey(key);
197+
let mp = [0, 0];
198+
let plaintext = "";
199+
for (const c of ciphertext) {
200+
let cp = findPos(key, c);
201+
const mix = findIx(findAtPos(key, mp));
202+
const pp = subPos(cp, mix);
203+
const p = findAtPos(key, pp);
204+
plaintext += p;
205+
key = rotateRight(key, pp[0], 1);
206+
cp = findPos(key, c);
207+
key = rotateDown(key, cp[1], 1);
208+
mp = addPos(mp, findIx(c));
209+
}
210+
return plaintext;
211+
}
212+
213+
/**
214+
* Adds padding to the input.
215+
*
216+
* @param {string} key
217+
* @param {string} plaintext
218+
* @param {string} signature
219+
* @param {number} paddingSize
220+
* @returns {string}
221+
*/
222+
export function encryptPad(key, plaintext, signature, paddingSize) {
223+
initTiles();
224+
checkKey(key);
225+
let padding = "";
226+
for (let i = 0; i < paddingSize; i++) {
227+
padding += letters.charAt(Math.floor(Math.random() * letters.length));
228+
}
229+
return encrypt(key, padding+plaintext+"---"+signature);
230+
}
231+
232+
/**
233+
* Removes padding from the ouput.
234+
*
235+
* @param {string} key
236+
* @param {string} ciphertext
237+
* @param {number} paddingSize
238+
* @returns {string}
239+
*/
240+
export function decryptPad(key, ciphertext, paddingSize) {
241+
initTiles();
242+
checkKey(key);
243+
return decrypt(key, ciphertext).slice(paddingSize);
244+
}

src/core/operations/LS47Decrypt.mjs

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @author n1073645 [[email protected]]
3+
* @copyright Crown Copyright 2020
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import * as LS47 from "../lib/LS47.mjs";
9+
10+
/**
11+
* LS47 Decrypt operation
12+
*/
13+
class LS47Decrypt extends Operation {
14+
15+
/**
16+
* LS47Decrypt constructor
17+
*/
18+
constructor() {
19+
super();
20+
21+
this.name = "LS47 Decrypt";
22+
this.module = "Crypto";
23+
this.description = "This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple key-expansion algorithm, because remembering passwords is popular. Similar security considerations as with ElsieFour hold.\nThe LS47 alphabet consists of following characters: _abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()\nA LS47 key is a permutation of the alphabet that is then represented in a 7x7 grid used for the encryption or decryption.";
24+
this.infoURL = "https://gitea.blesmrt.net/exa/ls47/src/branch/master";
25+
this.inputType = "string";
26+
this.outputType = "string";
27+
this.args = [
28+
{
29+
name: "Password",
30+
type: "string",
31+
value: ""
32+
},
33+
{
34+
name: "Padding",
35+
type: "number",
36+
value: 10
37+
}
38+
];
39+
}
40+
41+
/**
42+
* @param {string} input
43+
* @param {Object[]} args
44+
* @returns {string}
45+
*/
46+
run(input, args) {
47+
48+
this.paddingSize = parseInt(args[1], 10);
49+
50+
LS47.initTiles();
51+
52+
const key = LS47.deriveKey(args[0]);
53+
return LS47.decryptPad(key, input, this.paddingSize);
54+
}
55+
56+
}
57+
58+
export default LS47Decrypt;

src/core/operations/LS47Encrypt.mjs

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @author n1073645 [[email protected]]
3+
* @copyright Crown Copyright 2020
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import * as LS47 from "../lib/LS47.mjs";
9+
10+
/**
11+
* LS47 Encrypt operation
12+
*/
13+
class LS47Encrypt extends Operation {
14+
15+
/**
16+
* LS47Encrypt constructor
17+
*/
18+
constructor() {
19+
super();
20+
21+
this.name = "LS47 Encrypt";
22+
this.module = "Crypto";
23+
this.description = "This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple key-expansion algorithm, because remembering passwords is popular. Similar security considerations as with ElsieFour hold.\nThe LS47 alphabet consists of following characters: _abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()\nA LS47 key is a permutation of the alphabet that is then represented in a 7x7 grid used for the encryption or decryption.";
24+
this.infoURL = "https://gitea.blesmrt.net/exa/ls47/src/branch/master";
25+
this.inputType = "string";
26+
this.outputType = "string";
27+
this.args = [
28+
{
29+
name: "Password",
30+
type: "string",
31+
value: ""
32+
},
33+
{
34+
name: "Padding",
35+
type: "number",
36+
value: 10
37+
},
38+
{
39+
name: "Signature",
40+
type: "string",
41+
value: ""
42+
}
43+
];
44+
}
45+
46+
/**
47+
* @param {string} input
48+
* @param {Object[]} args
49+
* @returns {string}
50+
*/
51+
run(input, args) {
52+
53+
this.paddingSize = parseInt(args[1], 10);
54+
55+
LS47.initTiles();
56+
57+
const key = LS47.deriveKey(args[0]);
58+
return LS47.encryptPad(key, input, args[2], this.paddingSize);
59+
}
60+
61+
}
62+
63+
export default LS47Encrypt;

tests/operations/index.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import "./tests/SIGABA.mjs";
117117
import "./tests/ELFInfo.mjs";
118118
import "./tests/Subsection.mjs";
119119
import "./tests/CaesarBoxCipher.mjs";
120+
import "./tests/LS47.mjs";
120121

121122

122123
// Cannot test operations that use the File type yet

0 commit comments

Comments
 (0)