Skip to content

Commit

Permalink
#821 - random IV for CcAes
Browse files Browse the repository at this point in the history
  • Loading branch information
g4s8 committed Mar 14, 2018
1 parent 81818fd commit 7d17905
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 42 deletions.
100 changes: 73 additions & 27 deletions src/main/java/org/takes/facets/auth/codecs/CcAes.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,22 @@
/**
* AES codec which supports 128 bits key.
*
* <p>It's recommended to use it in conjunction with {@link CcSigned} codec,
* which can be applied
* <a href="https://crypto.stackexchange.com/a/205">before or after</a>
* encryption.
* <p>The class is immutable and thread-safe.
* @author Jason Wong (super132j@yahoo.com)
* @version $Id$
* @since 0.13.8
*/
@EqualsAndHashCode
public final class CcAes implements Codec {
/**
* Secure random instance.
*/
private static final SecureRandom RANDOM = new SecureRandom();

/**
* The block size constant.
*/
Expand All @@ -63,12 +72,11 @@ public final class CcAes implements Codec {
/**
* The encryption key.
*/
private final byte[] key;

private final Key key;
/**
* The algorithm parameter spec for cipher.
* Random.
*/
private final AlgorithmParameterSpec spec;
private final SecureRandom random;

/**
* Constructor for the class.
Expand All @@ -80,17 +88,37 @@ public final class CcAes implements Codec {
public CcAes(final Codec codec, final String key) {
this(codec, key.getBytes(Charset.defaultCharset()));
}

/**
* Constructor for the class.
*
* @param codec Original codec
* @param key The encryption key
*/
public CcAes(final Codec codec, final byte[] key) {
this(
codec,
CcAes.RANDOM,
new SecretKeySpec(
CcAes.withCorrectBlockSize(key.clone()), "AES"
)
);
}

/**
* Constructor for the class.
*
* @param codec Original codec
* @param random Random generator
* @param key The encryption key
*/
public CcAes(
final Codec codec,
final SecureRandom random,
final Key key
) {
this.origin = codec;
this.key = key.clone();
this.spec = CcAes.algorithmParameterSpec();
this.key = key;
this.random = random;
}

@Override
Expand All @@ -112,24 +140,27 @@ public Identity decode(final byte[] bytes) throws IOException {
*/
private byte[] encrypt(final byte[] bytes) throws IOException {
try {
return this.create(Cipher.ENCRYPT_MODE).doFinal(bytes);
final byte[] vector = new byte[CcAes.BLOCK];
this.random.nextBytes(vector);
final byte[] message = this.cipher(
Cipher.ENCRYPT_MODE,
new IvParameterSpec(vector)
).doFinal(bytes);
final byte[] res = new byte[vector.length + message.length];
System.arraycopy(vector, 0, res, 0, vector.length);
System.arraycopy(
message,
0,
res,
vector.length,
message.length
);
return res;
} catch (final BadPaddingException | IllegalBlockSizeException ex) {
throw new IOException(ex);
}
}

/**
* Create AlgorithmParameterSpec with the block size.
*
* @return The AlgorithmParameterSpec
*/
private static AlgorithmParameterSpec algorithmParameterSpec() {
final SecureRandom random = new SecureRandom();
final byte[] bytes = new byte[CcAes.BLOCK];
random.nextBytes(bytes);
return new IvParameterSpec(bytes);
}

/**
* Check the block size of the key.
*
Expand All @@ -156,8 +187,24 @@ private static byte[] withCorrectBlockSize(final byte[] key) {
* @throws IOException for all unexpected exceptions
*/
private byte[] decrypt(final byte[] bytes) throws IOException {
if (bytes.length < CcAes.BLOCK << 1) {
throw new DecodingException("Invalid encrypted message format");
}
try {
return this.create(Cipher.DECRYPT_MODE).doFinal(bytes);
final byte[] vector = new byte[CcAes.BLOCK];
final byte[] message = new byte[bytes.length - vector.length];
System.arraycopy(bytes, 0, vector, 0, vector.length);
System.arraycopy(
bytes,
vector.length,
message,
0,
message.length
);
return this.cipher(
Cipher.DECRYPT_MODE,
new IvParameterSpec(vector)
).doFinal(message);
} catch (final BadPaddingException | IllegalBlockSizeException ex) {
throw new DecodingException(ex);
}
Expand All @@ -167,20 +214,19 @@ private byte[] decrypt(final byte[] bytes) throws IOException {
* Create new cipher based on the valid mode from {@link Cipher} class.
*
* @param mode Either Cipher.ENRYPT_MODE or Cipher.DECRYPT_MODE
* @param spec Param spec (IV)
* @return The cipher
* @throws IOException For any unexpected exceptions
*/
private Cipher create(final int mode)
private Cipher cipher(final int mode, final AlgorithmParameterSpec spec)
throws IOException {
try {
final Key secret = new SecretKeySpec(
CcAes.withCorrectBlockSize(this.key), "AES"
);
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(mode, secret, this.spec);
cipher.init(mode, this.key, spec, this.random);
return cipher;
} catch (final InvalidKeyException | NoSuchAlgorithmException
| NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
| InvalidAlgorithmParameterException
| NoSuchPaddingException ex) {
throw new IOException(ex);
}
}
Expand Down
160 changes: 145 additions & 15 deletions src/test/java/org/takes/facets/auth/codecs/CcAesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
*/
package org.takes.facets.auth.codecs;

import java.io.IOException;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.util.Arrays;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Test;
Expand All @@ -35,8 +38,89 @@
* @author Jason Wong (super132j@yahoo.com)
* @version $Id$
* @since 0.13.8
* @checkstyle MagicNumber (500 line)
*/
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public final class CcAesTest {
/**
* CcAes can encrypt identity.
* @throws Exception If fails
*/
@Test
public void encryptIdentity() throws Exception {
final byte[] key = {
(byte) -25, (byte) 62, (byte) 118, (byte) 92,
(byte) -35, (byte) -24, (byte) 92, (byte) 48,
(byte) 5, (byte) -4, (byte) -88, (byte) -95,
(byte) -110, (byte) -54, (byte) 43, (byte) -1,
};
final byte[] random = {
(byte) 63, (byte) -27, (byte) -43, (byte) -52,
(byte) -70, (byte) -44, (byte) 86, (byte) -43,
(byte) -43, (byte) 116, (byte) -122, (byte) 105,
(byte) 108, (byte) -25, (byte) -126, (byte) 90,
};
final byte[] encrypted = new CcAes(
new CcTest(),
new CcAesTest.FkRandom(random),
new SecretKeySpec(key, "AES")
).encode(new Identity.Simple("urg:github:0000"));
MatcherAssert.assertThat(
"Encrypted identity does not start with IV",
Arrays.copyOf(encrypted, 16),
Matchers.equalTo(random)
);
final byte[] message = new byte[encrypted.length - 16];
System.arraycopy(encrypted, 16, message, 0, message.length);
MatcherAssert.assertThat(
"Encrypted message did not match",
message,
Matchers.equalTo(
new byte[]{
(byte) -119, (byte) -114, (byte) 19, (byte) 21,
(byte) 77, (byte) 59, (byte) 22, (byte) 100,
(byte) 121, (byte) -116, (byte) -43, (byte) 24,
(byte) 86, (byte) 24, (byte) -42, (byte) 119,
}
)
);
}

/**
* CcAes can decrypt identity.
* @throws Exception If fails
*/
@Test
public void decryptIdentity() throws Exception {
final byte[] encrypted = {
(byte) 83, (byte) -12, (byte) -8, (byte) 30,
(byte) -24, (byte) -5, (byte) -72, (byte) -33,
(byte) 13, (byte) 57, (byte) -37, (byte) -47,
(byte) -95, (byte) 108, (byte) 43, (byte) 101,
(byte) -87, (byte) -108, (byte) -41, (byte) 0,
(byte) 97, (byte) 1, (byte) -120, (byte) -39,
(byte) -114, (byte) 80, (byte) 18, (byte) -76,
(byte) -12, (byte) 10, (byte) -50, (byte) 51,
(byte) 66, (byte) 11, (byte) 13, (byte) -115,
(byte) 17, (byte) -41, (byte) -84, (byte) -78,
(byte) 48, (byte) 47, (byte) 42, (byte) -92,
(byte) -127, (byte) 16, (byte) -74, (byte) -61,
};
final byte[] key = {
(byte) 25, (byte) 92, (byte) 9, (byte) -75,
(byte) 54, (byte) 20, (byte) -118, (byte) 73,
(byte) -2, (byte) 81, (byte) 24, (byte) -5,
(byte) 20, (byte) 122, (byte) 92, (byte) -128,
};
MatcherAssert.assertThat(
new CcAes(
new CcTest(),
new SecureRandom(),
new SecretKeySpec(key, "AES")
).decode(encrypted).urn(),
Matchers.equalTo("urn:github:29835")
);
}

/**
* CcAES can encode and decode.
Expand All @@ -49,20 +133,7 @@ public void encodesAndDecodes() throws Exception {
generator.init(length);
final byte[] key = generator.generateKey().getEncoded();
final String plain = "This is a test!!@@**";
final Codec codec = new CcAes(
new Codec() {
@Override
public Identity decode(final byte[] bytes) throws IOException {
return new Identity.Simple(new String(bytes));
}
@Override
public byte[] encode(final Identity identity)
throws IOException {
return identity.urn().getBytes();
}
},
key
);
final Codec codec = new CcAes(new CcTest(), key);
MatcherAssert.assertThat(
codec.decode(codec.encode(new Identity.Simple(plain))).urn(),
Matchers.equalTo(plain)
Expand All @@ -79,4 +150,63 @@ public void throwsRightWhenBroken() throws Exception {
new CcPlain(), "0123456701234567"
).decode("broken input".getBytes());
}

/**
* Fake random with provided random result.
*/
private static final class FkRandom extends SecureRandom {
/**
* Serial id.
*/
private static final long serialVersionUID = 8646596235826414879L;
/**
* Ctor.
* @param fake Bytes
*/
FkRandom(final byte[] fake) {
super(new CcAesTest.FkRandomSpi(fake), null);
}
}

/**
* Fake random SPI.
*/
@SuppressWarnings("PMD.UncommentedEmptyMethodBody")
private static final class FkRandomSpi extends SecureRandomSpi {
/**
* Serial id.
*/
private static final long serialVersionUID = -5153681125995322457L;
/**
* Bytes.
*/
private final byte[] fake;
/**
* Ctor.
* @param fake Bytes
*/
FkRandomSpi(final byte[] fake) {
super();
this.fake = fake.clone();
}

@Override
public void engineSetSeed(final byte[] bytes) {
}

@Override
public void engineNextBytes(final byte[] bytes) {
if (bytes.length > this.fake.length) {
throw new UnsupportedOperationException(
"Byte-array is too big"
);
}
System.arraycopy(this.fake, 0, bytes, 0, bytes.length);
}

@Override
public byte[] engineGenerateSeed(final int length) {
return new byte[length];
}
}
}
Loading

0 comments on commit 7d17905

Please sign in to comment.