From 7d179059591e110dbab4b2e2075b22332fc022db Mon Sep 17 00:00:00 2001 From: g4s8 Date: Wed, 14 Mar 2018 13:38:59 +0300 Subject: [PATCH] #821 - random IV for CcAes --- .../org/takes/facets/auth/codecs/CcAes.java | 100 ++++++++--- .../takes/facets/auth/codecs/CcAesTest.java | 160 ++++++++++++++++-- .../org/takes/facets/auth/codecs/CcTest.java | 43 +++++ 3 files changed, 261 insertions(+), 42 deletions(-) create mode 100644 src/test/java/org/takes/facets/auth/codecs/CcTest.java diff --git a/src/main/java/org/takes/facets/auth/codecs/CcAes.java b/src/main/java/org/takes/facets/auth/codecs/CcAes.java index 74df4c980..189bbb107 100644 --- a/src/main/java/org/takes/facets/auth/codecs/CcAes.java +++ b/src/main/java/org/takes/facets/auth/codecs/CcAes.java @@ -43,6 +43,10 @@ /** * AES codec which supports 128 bits key. * + *

It's recommended to use it in conjunction with {@link CcSigned} codec, + * which can be applied + * before or after + * encryption. *

The class is immutable and thread-safe. * @author Jason Wong (super132j@yahoo.com) * @version $Id$ @@ -50,6 +54,11 @@ */ @EqualsAndHashCode public final class CcAes implements Codec { + /** + * Secure random instance. + */ + private static final SecureRandom RANDOM = new SecureRandom(); + /** * The block size constant. */ @@ -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. @@ -80,7 +88,6 @@ public final class CcAes implements Codec { public CcAes(final Codec codec, final String key) { this(codec, key.getBytes(Charset.defaultCharset())); } - /** * Constructor for the class. * @@ -88,9 +95,30 @@ public CcAes(final Codec codec, final String key) { * @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 @@ -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. * @@ -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); } @@ -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); } } diff --git a/src/test/java/org/takes/facets/auth/codecs/CcAesTest.java b/src/test/java/org/takes/facets/auth/codecs/CcAesTest.java index 9928f75cb..3f3a7cdb0 100644 --- a/src/test/java/org/takes/facets/auth/codecs/CcAesTest.java +++ b/src/test/java/org/takes/facets/auth/codecs/CcAesTest.java @@ -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; @@ -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. @@ -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) @@ -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]; + } + } } diff --git a/src/test/java/org/takes/facets/auth/codecs/CcTest.java b/src/test/java/org/takes/facets/auth/codecs/CcTest.java new file mode 100644 index 000000000..3e834856a --- /dev/null +++ b/src/test/java/org/takes/facets/auth/codecs/CcTest.java @@ -0,0 +1,43 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2014-2018 Yegor Bugayenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.takes.facets.auth.codecs; + +import org.takes.facets.auth.Identity; + +/** + * Test codec. + * @author Kirill (g4s8.public@gmail.com) + * @version $Id$ + * @since 1.11.1 + */ +final class CcTest implements Codec { + @Override + public Identity decode(final byte[] bytes) { + return new Identity.Simple(new String(bytes)); + } + @Override + public byte[] encode(final Identity identity) { + return identity.urn().getBytes(); + } +}