Skip to content

Instantly share code, notes, and snippets.

@snarkbait66
Created May 21, 2018 05:36
Show Gist options
  • Save snarkbait66/4f412354dc394036cdb7e053aab09b9d to your computer and use it in GitHub Desktop.
Save snarkbait66/4f412354dc394036cdb7e053aab09b9d to your computer and use it in GitHub Desktop.
ElsieFour (LC4) cipher in Java 8
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