Created
May 21, 2018 05:36
-
-
Save snarkbait66/4f412354dc394036cdb7e053aab09b9d to your computer and use it in GitHub Desktop.
ElsieFour (LC4) cipher in Java 8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.*; | |
import java.util.concurrent.ThreadLocalRandom; | |
import java.util.function.Function; | |
import static java.util.stream.Collectors.*; | |
import java.util.stream.IntStream; | |
public class ElsieFour { | |
// constants to map character values | |
private static final String ALPHABET = "#_23456789abcdefghijklmnopqrstuvwxyz"; | |
private static final Map<Character, Integer> CHAR_MAP; | |
// fill map for O(1) lookup of characters to value | |
static { | |
CHAR_MAP = IntStream.range(0, 36) | |
.boxed() | |
.collect(toMap(ALPHABET::charAt, Function.identity())); | |
} | |
private String key = ""; | |
private String input = ""; | |
private String nonce = ""; | |
private String header = ""; | |
private String signature = ""; | |
private String message = ""; | |
private EncodingType type; | |
enum EncodingType { | |
ENCRYPT, DECRYPT | |
} | |
class Tile { | |
char value; | |
int x; | |
int y; | |
final int vx; | |
final int vy; | |
Tile(char value, int x, int y) { | |
this.value = value; | |
this.x = x; | |
this.y = y; | |
vx = CHAR_MAP.get(value) % 6; | |
vy = CHAR_MAP.get(value) / 6; | |
} | |
void moveRight() { | |
y = (y + 1) % 6; | |
} | |
void moveDown() { | |
x = (x + 1) % 6; | |
} | |
} | |
private Map<Character, Tile> tiles = new HashMap<>(); | |
private char[][] board = new char[6][6]; | |
private Tile marker; | |
// | |
/** | |
* Generate a 36-character (288 bit) key for encryption/decryption matrix | |
* @return String key | |
*/ | |
public static String generateKey() { | |
List<Character> character = ALPHABET.chars() | |
.mapToObj(x -> (char) x) | |
.collect(toList()); | |
Collections.shuffle(character); | |
return character.stream() | |
.map(x -> Character.toString(x)) | |
.collect(joining()); | |
} | |
/** | |
* Generate nonce string of length n, with n >= 6 | |
* @param length int length | |
* @return String nonce | |
*/ | |
public static String generateNonce(int length) { | |
StringBuilder sb = new StringBuilder(); | |
if (length < 6) { | |
throw new IllegalArgumentException("Nonce must be at least 6 characters"); | |
} | |
for (int i = 0; i < length; i++) { | |
sb.append(ALPHABET.charAt(ThreadLocalRandom.current().nextInt(36))); | |
} | |
return sb.toString(); | |
} | |
/** | |
* Automatically sanitize text to meet requirements for LC4 encryption | |
* set to lowercase, change whitespace to underscore character | |
* change 1 and 0 to _one_ and _zero_ respectively | |
* change any other character not in LC4 alphabet to '#' | |
* @param s | |
* @return | |
*/ | |
public static String sanitizeText(String s) { | |
s = s.toLowerCase(); | |
s = s.replaceAll("\\s", "_"); | |
s = s.replace("1", "_one_"); | |
s = s.replace("0", "_zero_"); | |
s = s.replaceAll("[^#_2-9a-z]", "#"); | |
return s; | |
} | |
/** | |
* Create new instance of class. Use this first, then use | |
* chaining to add key and other info. | |
* @param type type of process desired, encrypt or decrypt | |
* @return new instance of ElsieFour | |
*/ | |
public static ElsieFour getInstance(EncodingType type) { | |
return new ElsieFour(type); | |
} | |
/** | |
* REQUIRED: 36-character key to use for process. | |
* @param key String key | |
* @return this for method chaining | |
*/ | |
public ElsieFour usingKey(String key) { | |
validateKey(key); | |
this.key = key; | |
return this; | |
} | |
/** | |
* REQUIRED: Message text encrypt/decrypt | |
* must contain only characters in 36-character alphabet | |
* @param input String input | |
* @return this for method chaining | |
*/ | |
public ElsieFour withMessage(String input) { | |
validate(input); | |
this.input = input; | |
return this; | |
} | |
/** | |
* OPTIONAL: Specify a 6+ length string as the 'nonce' which | |
* further randomizes the key state before encrypting the input | |
* text. This string is not included in the final encryption | |
* must contain only characters in 36-character alphabet | |
* @param nonce String of six characters or more | |
* @return this for method chaining | |
*/ | |
public ElsieFour withNonce(String nonce) { | |
if (nonce.length() < 6) { | |
throw new IllegalArgumentException("Nonce text too short. Must be at least 6 characters."); | |
} | |
validate(nonce); | |
this.nonce = nonce; | |
return this; | |
} | |
/** | |
* OPTIONAL: Specify a string as the 'header' which | |
* further randomizes the key state before encrypting the input | |
* text. This string is not included in the final encryption | |
* must contain only characters in 36-character alphabet | |
* @param header String of six characters or more | |
* @return this for method chaining | |
*/ | |
public ElsieFour withHeader(String header) { | |
validate(header); | |
this.header = header; | |
return this; | |
} | |
/** | |
* OPTIONAL: Specify a string as the 'signature' which is | |
* encrypted and added to the end of the encryption message | |
* on decrypt, is extracted from the input message | |
* and must match this given string | |
* must contain only characters in 36-character alphabet | |
* @param signature String signature | |
* @return this for method chaining | |
*/ | |
public ElsieFour withSignature(String signature) { | |
if (signature.length() < 10) throw new IllegalArgumentException("Signature text too short. Must be at least 10 characters."); | |
validate(signature); | |
this.signature = signature; | |
return this; | |
} | |
private ElsieFour(EncodingType type) { | |
this.type = type; | |
} | |
/** | |
* Returns 36-character key. | |
* This is useful when key has been generated randomly. | |
* @return key | |
*/ | |
public String getKey() { return key; } | |
/** | |
* Returns nonce string. | |
* This is useful when nonce has been generated randomly. | |
* @return nonce | |
*/ | |
public String getNonce() { return nonce; } | |
/** | |
* Returns message after processing | |
* @return processed message | |
*/ | |
public String getMessage() { return message; } | |
/** | |
* Perform process of encryption/decryption and return resulting message | |
* @return processed message | |
*/ | |
public String process() { | |
if (key.length() == 0 || input.length() == 0) { | |
throw new IllegalArgumentException("Key and Message must be specified."); | |
} | |
loadKey(key); | |
marker = tiles.get(board[0][0]); | |
message = type == EncodingType.ENCRYPT ? encrypt() : decrypt(); | |
return message; | |
} | |
private char encryptChar(char start) { | |
// get tile from character | |
Tile plain = tiles.get(start); | |
// get encoded tile based on marker's vector information | |
Tile cipher = getMove(plain.x, plain.y, marker.vx, marker.vy); | |
// permute matrix | |
rotateRow(plain.x); | |
rotateColumn(cipher.y); | |
// move marker based on encoded character's vectors | |
marker = getMove(marker.x, marker.y, cipher.vx, cipher.vy); | |
return cipher.value; | |
} | |
private char decryptChar(char start) { | |
Tile cipher = tiles.get(start); | |
Tile plain = getMove(cipher.x, cipher.y, -marker.vx, -marker.vy); | |
rotateRow(plain.x); | |
rotateColumn(cipher.y); | |
marker = getMove(marker.x, marker.y, cipher.vx, cipher.vy); | |
return plain.value; | |
} | |
private String encrypt() { | |
// run encryption sequence with nonce if available | |
// ignoring return values | |
if (nonce.length() >= 6) { | |
nonce.chars().forEach(x -> encryptChar((char) x)); | |
} | |
// run encryption sequence with header character if available | |
// ignoring return values | |
if (header.length() > 0) { | |
header.chars().forEach(x -> encryptChar((char) x)); | |
} | |
String message = input + signature; | |
return message.chars() | |
.map(x -> encryptChar((char) x)) | |
.mapToObj(x -> Character.toString((char) x)) | |
.collect(joining()); | |
} | |
private String decrypt() { | |
if (nonce.length() >= 6) { | |
nonce.chars().forEach(x -> encryptChar((char) x)); | |
} | |
if (header.length() > 0) { | |
header.chars().forEach(x -> encryptChar((char) x)); | |
} | |
String message = input.chars() | |
.map(x -> decryptChar((char) x)) | |
.mapToObj(x -> Character.toString((char) x)) | |
.collect(joining()); | |
if (signature.length() > 0 && | |
!signature.equals(message.substring(message.length() - signature.length()))) { | |
return "Signature does not match valid signature."; | |
} | |
return message.substring(0, message.length() - signature.length()); | |
} | |
private boolean valid(String s) { | |
return s.matches("[#_2-9a-z]+"); | |
} | |
private void validate(String s) { | |
if (!valid(s)) throw new IllegalArgumentException("Invalid characters in string:\n" + s); | |
} | |
private void validateKey(String s) { | |
if (s.length() != 36) throw new IllegalArgumentException("Key incorrect size. must be exactly 36 distinct characters."); | |
if (!valid(s)) throw new IllegalArgumentException("Invalid characters in key."); | |
// check for duplicates using XOR | |
int xorSum = s.chars() | |
.reduce(0, (x, y) -> x ^ y); | |
if (xorSum != 103) throw new IllegalArgumentException("Key must not contain duplicates."); | |
} | |
private void rotateRow(int row) { | |
char temp = board[row][5]; | |
for (int i = 5; i > 0; i--) { | |
board[row][i] = board[row][i - 1]; | |
tiles.get(board[row][i]).moveRight(); | |
} | |
board[row][0] = temp; | |
tiles.get(temp).moveRight(); | |
} | |
private void rotateColumn(int col) { | |
char temp = board[5][col]; | |
for (int i = 5; i > 0; i--) { | |
board[i][col] = board[i - 1][col]; | |
tiles.get(board[i][col]).moveDown(); | |
} | |
board[0][col] = temp; | |
tiles.get(temp).moveDown(); | |
} | |
private void loadKey(String key) { | |
for (int i = 0; i < key.length(); i++) { | |
board[i / 6][i % 6] = key.charAt(i); | |
tiles.put(key.charAt(i), new Tile(key.charAt(i), i / 6, i % 6)); | |
} | |
} | |
private Tile getMove(int row, int col, int dx, int dy) { | |
return tiles.get(board[Math.floorMod(row + dy, 6)][Math.floorMod(col + dx, 6)]); | |
} | |
@Override | |
public String toString() { | |
return "======= LC4 Ciphertext Processing Details =======\n" + | |
"Processing Type: " + type.name() + "\n" + | |
"Key: " + key + "\n" + | |
"Nonce: " + (nonce.length() > 0 ? nonce : "none") + "\n" + | |
"Input text: " + input + "\n" + | |
"Header: " + (header.length() > 0 ? header : "none") + "\n" + | |
"Signature: " + (signature.length() > 0 ? signature : "none") + "\n" + | |
"========================================\n" + | |
"Processed Message: " + message; | |
} | |
public static void main(String[] args) { | |
ElsieFour lc4 = ElsieFour.getInstance(EncodingType.ENCRYPT) | |
.usingKey(ElsieFour.generateKey()) | |
.withMessage(ElsieFour.sanitizeText("This is a test. This is only a test!\n 1234567890")) | |
.withNonce(ElsieFour.generateNonce(10)) | |
.withHeader("testing_header") | |
.withSignature("xxx_this_is_my_signature"); | |
String result = lc4.process(); | |
System.out.println(result); | |
System.out.println(lc4); | |
ElsieFour lc4_decrypt = ElsieFour.getInstance(EncodingType.DECRYPT) | |
.usingKey(lc4.getKey()) | |
.withMessage(lc4.getMessage()) | |
.withNonce(lc4.getNonce()) | |
.withHeader("testing_header") | |
.withSignature("xxx_this_is_my_signature"); | |
System.out.println(lc4_decrypt.process()); | |
System.out.println(lc4_decrypt); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment