Skip to content

Commit 312e401

Browse files
feat: Add Polybius cipher
1 parent b7b71fb commit 312e401

File tree

7 files changed

+328
-1
lines changed

7 files changed

+328
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ document.getElementById("t").innerHTML = this["cipher-collection"].wolfenbuettel
7979
- Affine
8080
- AER-256
8181
- ARMON-64
82+
- Polybius
8283

8384
## 📖 Documentation
8485

docs/ciphers/polybius.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Polybius
2+
3+
> The Polybius cipher, named by a greek philosopher, utilizes a 5x5 square
4+
(or 6x6 with numbers) called *Polybius square* to encrypt and decrypt messages.
5+
It contains the cipher alphabet, modified by the key (distinct letters of the key
6+
first, then the rest of the alphabet).
7+
8+
## Cipher behavior information
9+
10+
* Case sensitive? ❌
11+
* Deterministic? ✓
12+
* Alphabet: A-Z, numbers optionally
13+
* Characters not in alphabet will be: **omitted**, **preserved**, **throw an error (default)**
14+
15+
## Default options object
16+
17+
```
18+
const options = {
19+
key: '', // Custom key containing only cipher alphabet characters (duplicates allowed)
20+
withNumbers: false, // Use numbers as well (increases square size from 5 to 6)
21+
equalLetters: 'IJ', // Two letters used for substitution
22+
failOnUnknownCharacter: true, // Throw an error on unknown characters
23+
omitUnknownCharacter: true // If no error should be thrown, omit character?
24+
}
25+
```
26+
27+
**ATTENTION**: The `equalLetters` option will only be used when `withNumbers` is set to false.
28+
The second character set will be substituted with the first one (default: J will become I).
29+
This is necessary because a 5x5 square can only bear 25 characters.
30+
31+
## Usage
32+
33+
### Encoding
34+
35+
#### Default
36+
37+
```
38+
import { polybius } from 'cipher-collection'
39+
40+
41+
console.log(polybius.encode('AB')) // 11 12
42+
console.log(polybius.encode('a')) // 11
43+
```
44+
45+
#### Custom key
46+
47+
```
48+
import { polybius } from 'cipher-collection'
49+
50+
51+
console.log(polybius.encode('Ac', { key: 'cakeIsLie' })) // 12 11
52+
```
53+
54+
#### Custom substitution
55+
56+
```
57+
import { polybius } from 'cipher-collection'
58+
59+
60+
console.log(polybius.encode('IJ')) // 24 24
61+
console.log(polybius.encode('UV', { equalLetters: 'UV' })) // 51 51
62+
```
63+
64+
65+
66+
#### With numbers
67+
68+
```
69+
import { polybius } from 'cipher-collection'
70+
71+
72+
console.log(polybius.encode('Ac012', { withNumbers: true })) // 11 13 53 54 55
73+
74+
// No substitution anymore!
75+
console.log(polybius.encode('IJ')) // 23 24
76+
```
77+
78+
79+
#### Error handling
80+
81+
```
82+
import { polybius } from 'cipher-collection'
83+
84+
console.log(polybius.encode('N*') // Error: Unknown character *
85+
console.log(polybius.encode('N*', { failOnUnknownCharacter: false })) // 33
86+
console.log(polybius.encode('N* ', { failOnUnknownCharacter: false, omitUnknownCharacter: false })) // 33 *
87+
```
88+
89+
90+
### Decoding
91+
92+
#### Default
93+
94+
```
95+
import { polybius } from 'cipher-collection'
96+
97+
98+
console.log(polybius.decode('11 12')) // AB
99+
```
100+
101+
#### Custom key
102+
103+
```
104+
import { polybius } from 'cipher-collection'
105+
106+
107+
console.log(polybius.decode('12 11', { key: 'cakeIsLie' })) // AC
108+
```
109+
110+
#### Custom substitution
111+
112+
```
113+
import { polybius } from 'cipher-collection'
114+
115+
116+
console.log(polybius.decode('24 24')) // I I
117+
console.log(polybius.decode('51', { equalLetters: 'UV' })) // U
118+
console.log(polybius.decode('51', { equalLetters: 'VU' })) // V
119+
```
120+
121+
122+
123+
#### With numbers
124+
125+
```
126+
import { polybius } from 'cipher-collection'
127+
128+
129+
console.log(polybius.decode('11 13 53 54 55', { withNumbers: true })) // AC012
130+
131+
// No substitution anymore!
132+
console.log(polybius.decode('23 24')) // IJ
133+
```
134+
135+
136+
#### Error handling
137+
138+
```
139+
import { polybius } from 'cipher-collection'
140+
141+
console.log(polybius.decode('33*') // Error: Unknown character *
142+
console.log(polybius.decode('33*', { failOnUnknownCharacter: false })) // N
143+
console.log(polybius.decode('33* ', { failOnUnknownCharacter: false, omitUnknownCharacter: false })) // N *
144+
```
145+

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
* [Affine](./ciphers/affine.md)
2121
* [AER-256](./ciphers/aer256.md)
2222
* [ARMON-64](./ciphers/armon64.md)
23+
* [Polybius](./ciphers/polybius.md)

src/helpers/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ export const randomInRange = (min, max) => {
88
return Math.floor(Math.random() * (max - min)) + min
99
}
1010

11+
/**
12+
* Throw an error or return an empty string silently
13+
* @param {object} options Object that **must** include a property called `failOnUnknownCharacter` which determines
14+
* if an error will be thrown or not
15+
* @param {string} errorMessage The error message that will be possibly used
16+
* @returns {string}
17+
*/
1118
export const throwOrSilent = (options, errorMessage) => {
1219
if (options.failOnUnknownCharacter) {
1320
throw Error(errorMessage)

src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import multiplicative from './multiplicative'
1010
import affine from './affine'
1111
import aer256 from './aer256'
1212
import armon64 from './armon64'
13+
import polybius from './polybius'
1314

1415
export default {
1516
rot,
@@ -23,5 +24,6 @@ export default {
2324
multiplicative,
2425
affine,
2526
aer256,
26-
armon64
27+
armon64,
28+
polybius
2729
}

src/polybius.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { throwOrSilent } from './helpers'
2+
3+
const encode = (input, options = {}) => {
4+
options = { ...DEFAULT_OPTIONS, ...options }
5+
6+
const grid = prepareGridString(options)
7+
let squareSize = 6
8+
9+
if (!options.withNumbers) {
10+
const [substitute, toReplace] = [...options.equalLetters]
11+
input = input.replace(new RegExp(toReplace, 'gi'), substitute)
12+
13+
squareSize = 5
14+
}
15+
return [...input.toUpperCase()].map(c => {
16+
/*
17+
* Think of a grid string as a real grid with 5^2 places:
18+
*
19+
* - 1 2 3 4 5
20+
* 1 A B C D E
21+
* 2 F G H I K
22+
* 3 L M N O P
23+
* 4 Q R S T U
24+
* 5 V W X Y Z
25+
*
26+
* Now map letters to the corresponding row and col
27+
*/
28+
29+
const characterIndex = grid.indexOf(c)
30+
const row = Math.ceil((characterIndex + 1) / squareSize)
31+
const col = (characterIndex % squareSize) + 1
32+
33+
if ([row, col].includes(0)) {
34+
return errorExpression(c, options)
35+
}
36+
37+
return `${row}${col}`
38+
}).filter(c => c.length).join(' ')
39+
}
40+
41+
const decode = (input, options = {}) => {
42+
options = { ...DEFAULT_OPTIONS, ...options }
43+
44+
const grid = prepareGridString(options)
45+
let squareSize = options.withNumbers ? 6 : 5
46+
47+
return input.split(' ')
48+
.map(sequence => {
49+
if (!sequence.match(/\d{2}/)) {
50+
return errorExpression(sequence, options)
51+
}
52+
const [row, col] = sequence
53+
54+
return grid.charAt(squareSize * (row - 1) + (col - 1)) || errorExpression(sequence, options)
55+
}).join('')
56+
}
57+
58+
const errorExpression = (c, options) => throwOrSilent(options, `Unknown character ${c}`) || options.omitUnknownCharacter ? '' : c
59+
60+
const DEFAULT_OPTIONS = {
61+
key: '',
62+
equalLetters: 'IJ',
63+
withNumbers: false,
64+
failOnUnknownCharacter: true,
65+
omitUnknownCharacter: true
66+
}
67+
68+
const prepareGridString = options =>
69+
[...options.key.toUpperCase(), ...getAlphabet(options)].reduce((acc, letter) => {
70+
return !acc.includes(letter) ? acc.concat(letter) : acc
71+
})
72+
73+
const getAlphabet = options => {
74+
const [substitute, toReplace] = [...options.equalLetters]
75+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
76+
77+
return options.withNumbers ? `${alphabet}0123456789` : alphabet.replace(toReplace, substitute)
78+
}
79+
80+
export default {
81+
encode,
82+
decode
83+
}

test/polybius.test.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import polybius from 'polybius'
2+
3+
describe('encoding', () => {
4+
test('default', () => {
5+
expect(polybius.encode('ABCDEFGHIJKLMNOPQRSTUVWXYZ')).toBe('11 12 13 14 15 21 22 23 24 24 25 31 32 33 34 35 41 42' +
6+
' 43 44 45 51 52 53 54 55')
7+
expect(polybius.encode('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.toLowerCase())).toBe('11 12 13 14 15 21 22 23 24 24 25 31 32 33 34 35' +
8+
' 41 42' +
9+
' 43 44 45 51 52 53 54 55')
10+
})
11+
test('default with illegal character', () => {
12+
expect(() => { polybius.encode('*') }).toThrowError('Unknown character *')
13+
})
14+
15+
test('with custom key', () => {
16+
expect(polybius.encode('ABCDEFGHIJKLMNOPQRSTUVWXYZ', { key: 'OHA' }))
17+
.toBe('13 14 15 21 22 23 24 12 25 25 31 32 33 34 11 35 41 42 43 44 45 51 52 53 54 55')
18+
expect(polybius.encode('ABCDEFGHIJKLMNOPQRSTUVWXYZ', { key: 'oHa' }))
19+
.toBe('13 14 15 21 22 23 24 12 25 25 31 32 33 34 11 35 41 42 43 44 45 51 52 53 54 55')
20+
})
21+
22+
test('with numbers', () => {
23+
expect(polybius.encode('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', { withNumbers: true }))
24+
.toBe('11 12 13 14 15 16 21 22 23 24 25 26 31 32 33 34 35 36 41 42 43 44 45 46 51 52 53 54 55 56 61 62 63 64' +
25+
' 65 66')
26+
})
27+
28+
test('with numbers and custom key', () => {
29+
expect(polybius.encode('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', { withNumbers: true, key: 'oHa' }))
30+
.toBe('13 14 15 16 21 22 23 12 24 25 26 31 32 33 11 34 35 36 41 42 43 44 45 46 51 52 53 54 55 56 61 62 63 64 65 66')
31+
})
32+
33+
test('with custom substitution', () => {
34+
expect(polybius.encode('ABCDEFGHIJKLMNOPQRSTUVWXYZ', { equalLetters: 'UV' }))
35+
.toBe('11 12 13 14 15 21 22 23 24 25 31 32 33 34 35 41 42 43 44 45 51 51 52 53 54 55')
36+
})
37+
38+
test('omit illegal character', () => {
39+
expect(polybius.encode('N*', { failOnUnknownCharacter: false })).toBe('33')
40+
})
41+
42+
test('preserve illegal character', () => {
43+
expect(polybius.encode('N* ', { failOnUnknownCharacter: false, omitUnknownCharacter: false })).toBe('33 * ')
44+
})
45+
})
46+
47+
describe('decoding', () => {
48+
test('default', () => {
49+
expect(polybius.decode('11 12 13 14 15 21 22 23 24 24 25 31 32 33 34 35 41 42 43 44 45 51 52 53 54 55')).toBe('ABCDEFGHIIKLMNOPQRSTUVWXYZ')
50+
})
51+
test('default with illegal character', () => {
52+
expect(() => { polybius.decode('*') }).toThrowError('Unknown character *')
53+
expect(() => { polybius.decode('88') }).toThrowError('Unknown character 88')
54+
})
55+
56+
test('with custom key', () => {
57+
expect(polybius.decode('13 14 15 21 22 23 24 12 25 25 31 32 33 34 11 35 41 42 43 44 45 51 52 53 54 55', { key: 'OHA' }))
58+
.toBe('ABCDEFGHIIKLMNOPQRSTUVWXYZ')
59+
expect(polybius.decode('13 14 15 21 22 23 24 12 25 25 31 32 33 34 11 35 41 42 43 44 45 51 52 53 54 55', { key: 'oHa' }))
60+
.toBe('ABCDEFGHIIKLMNOPQRSTUVWXYZ')
61+
})
62+
63+
test('with numbers', () => {
64+
expect(polybius.decode('11 12 13 14 15 16 21 22 23 24 25 26 31 32 33 34 35 36 41 42 43 44 45 46 51 52 53 54 55 56 61 62 63 64 65 66', { withNumbers: true }))
65+
.toBe('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
66+
})
67+
68+
test('with numbers and custom key', () => {
69+
expect(polybius.decode('13 14 15 16 21 22 23 12 24 25 26 31 32 33 11 34 35 36 41 42 43 44 45 46 51 52 53 54 55' +
70+
' 56 61 62 63 64 65 66', { withNumbers: true, key: 'oHa' }))
71+
.toBe('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
72+
})
73+
74+
test('with custom substitution', () => {
75+
expect(polybius.decode('11 12 13 14 15 21 22 23 24 25 31 32 33 34 35 41 42 43 44 45 51 51 52 53 54 55', { equalLetters: 'UV' }))
76+
.toBe('ABCDEFGHIJKLMNOPQRSTUUWXYZ')
77+
expect(polybius.decode('11 12 13 14 15 21 22 23 24 25 31 32 33 34 35 41 42 43 44 45 51 51 52 53 54 55', { equalLetters: 'VU' }))
78+
.toBe('ABCDEFGHIJKLMNOPQRSTVVWXYZ')
79+
})
80+
81+
test('omit illegal character', () => {
82+
expect(polybius.decode('33 *', { failOnUnknownCharacter: false })).toBe('N')
83+
})
84+
85+
test('preserve illegal character', () => {
86+
expect(polybius.decode('33 *', { failOnUnknownCharacter: false, omitUnknownCharacter: false })).toBe('N*')
87+
})
88+
})

0 commit comments

Comments
 (0)