An easy algorithm for encrypting and decrypting binary data using a cipher key in Java

I have this easy en-/decryption algorithm.

Disclaimer

However, I have absolutely no prior experience in information security, encryption, and so on, so bare with me.

Encryption

Encryption works this way: first we read in four first bytes from the data being encrypted; then, we treat the four bytes as a single 32-bit integer and we add the value of the cipher to that value; then, we store the sum into the same location (four first bytes). Next we "shift the window" one byte to the right and read once again a 32-bit value that, however, starts at the byte 1 (indexing starts from 0); add cipher, store, and so on.

Decryption

The decryption algorithm undoes what encryption method does: it begins from the four last bytes; reads them, subtracts the cipher key, and stores back, moves the window one byte towards beginning of the data array.

The code follows:

CipherTools.java:

package net.coderodde.encryption;

import java.util.Arrays;
import java.util.Random;

/**
* This class provides static methods for encrypting and decrypting binary data
* represented by arrays of bytes.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Feb 29, 2016)
*/
public class CipherTools {

private static final int BYTES_PER_INT = 4;

/**
* Encrypts the input data {@code input} using the cipher key
* {@code cipherKey}.
*
* @param  input     the data to encrypt.
* @param  cipherKey the cipher key.
* @return           the encrypted data.
*/
public static byte[] encrypt(byte[] input, int cipherKey) {
checkCipherNotZero(cipherKey);
byte[] output = input.clone();

for (int i = 0; i <= output.length - BYTES_PER_INT; ++i) {
writeInt(output, i, readInt(output, i) + cipherKey);
}

return output;
}

/**
* Decrypts the input data {@code input} using the cipher key
* {@code cipherKey}.
*
* @param  input     the input data to decrypt.
* @param  cipherKey the cipher key.
* @return           the decrypted data.
*/
public static byte[] decrypt(byte[] input, int cipherKey) {
checkCipherNotZero(cipherKey);
byte[] output = input.clone();

for (int i = output.length - BYTES_PER_INT; i >= 0; --i) {
writeInt(output, i, readInt(output, i) - cipherKey);
}

return output;
}

private static void checkCipherNotZero(int cipherKey) {
if (cipherKey == 0) {
throw new IllegalArgumentException(
"The input cipher key is zero. For this reason, the data " +
"would not be encrypted.");
}
}

/**
* Returns the integer represented by bytes {@code data[offset],
* data[offset + 1], data[offset + 2], data[offset + 3]}, where the bytes
* are listed from least significant to most significant.
*
* @param data   the data array holding the bytes.
* @param offset the offset of the integer to read.
* @return       a four byte integer value.
*/
private static int readInt(byte[] data, int offset) {
int b0 = Byte.toUnsignedInt(data[offset]);
int b1 = Byte.toUnsignedInt(data[offset + 1]);
int b2 = Byte.toUnsignedInt(data[offset + 2]);
int b3 = Byte.toUnsignedInt(data[offset + 3]);

return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
}

/**
* Writes the value {@code value} to the byte array {@code data} starting
* from index {@code offset}, or namely, to the bytes {@code data[offset],
* data[offset + 1], data[offset + 2], data[offset + 3]}, where the least
* significant byte of the value is stored in the byte {@code data[offset]},
* i.e., we assume a <b>little-endian</b> machine.
*
* @param data   the array holding the data to write to.
* @param offset the index of the least significant byte of the target
*               data integer.
* @param value  the value to write.
*/
private static void writeInt(byte[] data, int offset, int value) {
data[offset] = (byte)(value & 0xff);
data[offset + 1] = (byte)((value >>> 8) & 0xff);
data[offset + 2] = (byte)((value >>> 16) & 0xff);
data[offset + 3] = (byte)((value >>> 24) & 0xff);
}

public static void main(final String... args) {
Random random = new Random();
byte[] before = new byte[10];
random.nextBytes(before);

int cipherKey = random.nextInt();
byte[] encrypted = encrypt(before, cipherKey);
byte[] after = decrypt(encrypted, cipherKey);

System.out.println("Before:    " + Arrays.toString(before));
System.out.println("Encrypted: " + Arrays.toString(encrypted));
System.out.println("After:     " + Arrays.toString(after));
System.out.println("Match: " + Arrays.equals(before, after));
}
}


CipherToolsTest.java:

package net.coderodde.encryption;

import java.util.Arrays;
import java.util.Random;
import org.junit.Test;
import static org.junit.Assert.*;

public class CipherToolsTest {

private static final int ITERATIONS = 100;
private static final int MAXIMUM_LENGTH = 1000;

@Test
public void testEncryptionDecryption() {
long seed = System.nanoTime();
Random random = new Random(seed);
System.out.println("Seed = " + seed);

for (int iteration = 0; iteration < ITERATIONS; iteration++) {
int cipherKey = random.nextInt();

if (cipherKey == 0) {
cipherKey = 1;
}

byte[] before = new byte[random.nextInt(MAXIMUM_LENGTH + 1)];
random.nextBytes(before);

byte[] encrypted = CipherTools.encrypt(before, cipherKey);
byte[] after = CipherTools.decrypt(encrypted,  cipherKey);

assertTrue(Arrays.equals(before, after));
assertFalse(Arrays.equals(before, encrypted));
}
}

@Test(expected = IllegalArgumentException.class)
public void testEncryptionThrowsOnZeroCipher() {
CipherTools.encrypt(new byte[2], 0);
}

@Test(expected = IllegalArgumentException.class)
public void testDecryptionThrowsOnZeroCipher() {
CipherTools.decrypt(new byte[2], 0);
}
}


Please, tell me anything that comes to mind. Also, is it easy to crack that cipher if the hacker, say, knows that the file encrypted is a source code file in some particular language?

First of all, I'm going to assume that this is for educational purposes only. If not, see Don't roll your own and Don't be a Dave.

What you have is basically a variant of the caesar cipher (well, more or less). You take each character of the input and add a fixed amount (the key) to it. Some parts of the output also depend on previous calculations, but not all of them (not at the beginning or at the end).

I haven't yet looked into it in-depth, but the first problems I saw are:

• your key size is incredibly small, as it is limited by the size of int. This makes brute force attacks simple[*].
• a chosen plaintext attack is possible to gain the key: If the message that is to be encrypted is the same length as BYTES_PER_INT, the key can be computed: int key = readInt(ciphertext, ciphertext.length - BYTES_PER_INT) - readInt(plaintext, plaintext.length - BYTES_PER_INT). This is because in that case, the algorithm is indeed just a simple ceasar cipher.
• for small keys, a known plaintext attack is possible. An attacker could get the key if they have access to a plaintext and a matching cipher, as they can just calculate the offset of the first value of the ciphertext and the plaintext, and thus gain the key (actually, I think that a known plaintext attack should be possible regardless of key length, but for some reason I can't think of a way right now).
• as you are not padding, the last X values of the ciphertext are always equal to the plaintext.

So no, this would not be secure. And there may very likely be more severe problems than the ones I described. But even those are enough to show that there are problems in the algorithm.

[*]

Also, is it easy to crack that cipher if the hacker, say, knows that the file encrypted is a source code file in some particular language?

Yes, it will be simple for an attacker to know when they used a correct key.

I'm assuming that you would encode a message like this: message.getBytes(), and then get it back from a cipher via new String(cipher). You can test this and see that a wrong key will give you a lot of non-ascii characters, making a distinction between correct and incorrect decryption easy.

Misc

• A cipher key doesn't really exist. Just key is fine.
• your JavaDoc comments are quite nice. They are well formatted and contain all the information needed to understand what a method does.

Observe that there is no point writing all four bytes of the result: three of them will be overwritten anyway. Only the least significant one will remain in the output. This also means that there is no point to compute anything but the least significant byte. So the encryption effectively degenerates to a substitution cypher, modulo 3 bytes of lead-in and lead-out.

For example, hello my dear world and hello dear my world are encrypted respectively to

-24, 61, -42, -46, -43, -122, -45, -33, -122, -54, -53, -57, -40, -122, -35, -43, 88, -6, 96

-24, 61, -42, -46, -43, -122, -54, -53, -57, -40, -122, -45, -33, -122, -35, -43, 88, -6, 96


Factoring out hello and world observe 1:1 correspondence between letters and ciphers:

 m    y    _     d    e    a    r
-45, -33, -122, -54, -53, -57, -40
d    e    a    r    _     m    y
-54, -53, -57, -40, -122, -45, -33


All known weaknesses of substitutional cipher apply.