From 020b529d9bb43e7174a7b65d0f1b9827bb8a3fe9 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 27 Nov 2020 18:50:53 -0600 Subject: [PATCH] Add data security API This relates to #4. --- .../test/java/dev/o1c/SecretKeySealTest.java | 81 +++++++++ java8/src/main/java/dev/o1c/DataSecurity.java | 38 ++++ java8/src/main/java/dev/o1c/Seal.java | 31 ++++ java8/src/main/java/dev/o1c/SealedData.java | 37 ++++ .../src/main/java/dev/o1c/SecretKeySeal.java | 168 ++++++++++++++++++ java8/src/main/java/dev/o1c/TokenSeal.java | 31 ++++ java8/src/main/java/dev/o1c/spi/ByteOps.java | 4 + 7 files changed, 390 insertions(+) create mode 100644 java15/src/test/java/dev/o1c/SecretKeySealTest.java create mode 100644 java8/src/main/java/dev/o1c/DataSecurity.java create mode 100644 java8/src/main/java/dev/o1c/Seal.java create mode 100644 java8/src/main/java/dev/o1c/SealedData.java create mode 100644 java8/src/main/java/dev/o1c/SecretKeySeal.java create mode 100644 java8/src/main/java/dev/o1c/TokenSeal.java diff --git a/java15/src/test/java/dev/o1c/SecretKeySealTest.java b/java15/src/test/java/dev/o1c/SecretKeySealTest.java new file mode 100644 index 0000000..676c5c9 --- /dev/null +++ b/java15/src/test/java/dev/o1c/SecretKeySealTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 Matt Sicker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.o1c; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +import static dev.o1c.spi.Algorithm.ChaCha20Poly1305; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class SecretKeySealTest { + + private static Random random; + + private TokenSeal seal; + + @BeforeAll + static void beforeAll() throws NoSuchAlgorithmException { + random = SecureRandom.getInstanceStrong(); + } + + @BeforeEach + void setUp() { + var key = new byte[ChaCha20Poly1305.getKeySize()]; + random.nextBytes(key); + seal = DataSecurity.sealWithKey(key); + } + + @Test + void sealNoContext() { + var plaintext = new byte[4096]; + random.nextBytes(plaintext); + assertArrayEquals(plaintext, seal.unseal(seal.seal(plaintext))); + } + + @Test + void sealWithContext() { + var plaintext = new byte[420]; + random.nextBytes(plaintext); + var context = new byte[42]; + random.nextBytes(context); + assertArrayEquals(plaintext, seal.unseal(seal.seal(plaintext, context), context)); + } + + @Test + void tokenSealNoContext() { + var plaintext = new byte[2043]; + random.nextBytes(plaintext); + var secureData = seal.tokenSeal(plaintext); + assertArrayEquals(plaintext, seal.tokenUnseal(secureData.getEncryptedData(), secureData.getToken())); + } + + @Test + void tokenSealWithContext() { + var plaintext = new byte[1023]; + random.nextBytes(plaintext); + var context = new byte[63]; + random.nextBytes(context); + var secureData = seal.tokenSeal(plaintext, context); + assertArrayEquals(plaintext, seal.tokenUnseal(secureData.getEncryptedData(), secureData.getToken(), context)); + } +} diff --git a/java8/src/main/java/dev/o1c/DataSecurity.java b/java8/src/main/java/dev/o1c/DataSecurity.java new file mode 100644 index 0000000..0bf9dc7 --- /dev/null +++ b/java8/src/main/java/dev/o1c/DataSecurity.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Matt Sicker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.o1c; + +import dev.o1c.spi.Algorithm; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.Objects; + +public class DataSecurity { + public static TokenSeal sealWithKey(byte[] key) { + Algorithm algorithm = Algorithm.ChaCha20Poly1305; + if (key.length != algorithm.getKeySize()) { + throw new IllegalArgumentException( + "Keys must be " + algorithm.getKeySize() + " bytes but got " + key.length + " bytes"); + } + return sealWithKey(new SecretKeySpec(key, algorithm.getAlgorithm())); + } + + public static TokenSeal sealWithKey(SecretKey key) { + return new SecretKeySeal(Objects.requireNonNull(key)); + } +} diff --git a/java8/src/main/java/dev/o1c/Seal.java b/java8/src/main/java/dev/o1c/Seal.java new file mode 100644 index 0000000..3be9d67 --- /dev/null +++ b/java8/src/main/java/dev/o1c/Seal.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Matt Sicker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.o1c; + +public interface Seal { + byte[] seal(byte[] data, byte[] context); + + default byte[] seal(byte[] data) { + return seal(data, null); + } + + byte[] unseal(byte[] sealedData, byte[] context); + + default byte[] unseal(byte[] sealedData) { + return unseal(sealedData, null); + } +} diff --git a/java8/src/main/java/dev/o1c/SealedData.java b/java8/src/main/java/dev/o1c/SealedData.java new file mode 100644 index 0000000..057e11b --- /dev/null +++ b/java8/src/main/java/dev/o1c/SealedData.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Matt Sicker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.o1c; + +import java.util.Objects; + +public final class SealedData { + private final byte[] encryptedData; + private final byte[] token; + + public SealedData(byte[] encryptedData, byte[] token) { + this.encryptedData = Objects.requireNonNull(encryptedData); + this.token = Objects.requireNonNull(token); + } + + public byte[] getEncryptedData() { + return encryptedData; + } + + public byte[] getToken() { + return token; + } +} diff --git a/java8/src/main/java/dev/o1c/SecretKeySeal.java b/java8/src/main/java/dev/o1c/SecretKeySeal.java new file mode 100644 index 0000000..51e5fdb --- /dev/null +++ b/java8/src/main/java/dev/o1c/SecretKeySeal.java @@ -0,0 +1,168 @@ +/* + * Copyright 2020 Matt Sicker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.o1c; + +import dev.o1c.spi.Algorithm; +import dev.o1c.spi.ByteOps; +import dev.o1c.spi.InvalidProviderException; + +import javax.crypto.AEADBadTagException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Objects; + +class SecretKeySeal implements TokenSeal { + private static final int TAG_SIZE = 16; + private static final int NONCE_SIZE = 12; + private static final int TOKEN_TYPE = 0x43433230; // CC20 in ASCII, big endian order + private static final int TOKEN_SIZE = NONCE_SIZE + TAG_SIZE + Integer.BYTES; + // token = tag [0..15] + nonce [16..27] + token_type [28..31] + + private final SecretKey key; + + SecretKeySeal(SecretKey key) { + this.key = key; + } + + @Override + public byte[] seal(byte[] data, byte[] context) { + Objects.requireNonNull(data); + Cipher cipher = initEncrypt(context); + byte[] nonce = cipher.getIV(); + ByteBuffer sealedData = ByteBuffer.allocate(cipher.getOutputSize(data.length) + nonce.length + Integer.BYTES); + try { + cipher.doFinal(ByteBuffer.wrap(data), sealedData); + } catch (BadPaddingException | IllegalBlockSizeException | ShortBufferException e) { + throw new IllegalStateException(e); + } + sealedData.put(nonce); + sealedData.putInt(TOKEN_TYPE); + return sealedData.array(); + } + + // TODO: consider propagating checked AEADBadTagException + @Override + public byte[] unseal(byte[] sealedData, byte[] context) { + Objects.requireNonNull(sealedData); + int typeOffset = sealedData.length - Integer.BYTES; + int tokenType = ByteOps.unpackInt(sealedData, typeOffset); + if (tokenType != TOKEN_TYPE) { + throw new IllegalArgumentException("Unsupported seal type detected: " + Integer.toHexString(tokenType)); + } + int nonceOffset = typeOffset - NONCE_SIZE; + IvParameterSpec nonce = new IvParameterSpec(sealedData, nonceOffset, NONCE_SIZE); + try { + return initDecrypt(nonce, context).doFinal(sealedData, 0, nonceOffset); + } catch (AEADBadTagException e) { + throw new O1CException(e); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new IllegalStateException(e); + } + } + + @Override + public SealedData tokenSeal(byte[] data, byte[] context) { + Objects.requireNonNull(data); + Cipher cipher = initEncrypt(context); + byte[] nonce = cipher.getIV(); + ByteBuffer ciphertext = ByteBuffer.allocate(cipher.getOutputSize(data.length)); + try { + cipher.doFinal(ByteBuffer.wrap(data), ciphertext); + } catch (BadPaddingException | IllegalBlockSizeException | ShortBufferException e) { + throw new IllegalStateException(e); + } + ciphertext.flip(); + byte[] encryptedData = new byte[data.length]; + ciphertext.get(encryptedData); + ByteBuffer token = ByteBuffer.allocate(TOKEN_SIZE); + token.put(ciphertext); + token.put(nonce); + token.putInt(TOKEN_TYPE); + return new SealedData(encryptedData, token.array()); + } + + @Override + public byte[] tokenUnseal(byte[] encryptedData, byte[] token, byte[] context) { + Objects.requireNonNull(encryptedData); + Objects.requireNonNull(token); + if (token.length != TOKEN_SIZE) { + throw new IllegalArgumentException("Token size must be " + TOKEN_SIZE + " bytes"); + } + int tokenType = ByteOps.unpackInt(token, TAG_SIZE + NONCE_SIZE); + if (tokenType != TOKEN_TYPE) { + throw new IllegalArgumentException("Unsupported seal token type detected: " + Integer.toHexString(tokenType)); + } + IvParameterSpec nonce = new IvParameterSpec(token, TAG_SIZE, NONCE_SIZE); + Cipher cipher = initDecrypt(nonce, context); + byte[] plaintext = new byte[encryptedData.length]; + try { + cipher.doFinal(token, 0, TAG_SIZE, plaintext, + cipher.update(encryptedData, 0, encryptedData.length, plaintext)); + } catch (AEADBadTagException e) { + throw new O1CException(e); + } catch (BadPaddingException | IllegalBlockSizeException | ShortBufferException e) { + throw new IllegalStateException(e); + } + return plaintext; + } + + private Cipher initEncrypt(byte[] context) { + Cipher cipher = createCipher(); + try { + cipher.init(Cipher.ENCRYPT_MODE, key, SecureRandom.getInstanceStrong()); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException(e); + } catch (NoSuchAlgorithmException e) { + throw new InvalidProviderException(e); + } + if (context != null && context.length > 0) { + cipher.updateAAD(context); + } + return cipher; + } + + private Cipher initDecrypt(IvParameterSpec nonce, byte[] context) { + Cipher cipher = createCipher(); + try { + cipher.init(Cipher.DECRYPT_MODE, key, nonce); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException(e); + } + if (context != null && context.length > 0) { + cipher.updateAAD(context); + } + return cipher; + } + + private static Cipher createCipher() { + try { + return Cipher.getInstance(Algorithm.ChaCha20Poly1305.getAlgorithm()); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new InvalidProviderException(e); + } + } +} diff --git a/java8/src/main/java/dev/o1c/TokenSeal.java b/java8/src/main/java/dev/o1c/TokenSeal.java new file mode 100644 index 0000000..97edd5f --- /dev/null +++ b/java8/src/main/java/dev/o1c/TokenSeal.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Matt Sicker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.o1c; + +public interface TokenSeal extends Seal { + SealedData tokenSeal(byte[] data, byte[] context); + + default SealedData tokenSeal(byte[] data) { + return tokenSeal(data, null); + } + + byte[] tokenUnseal(byte[] encryptedData, byte[] token, byte[] context); + + default byte[] tokenUnseal(byte[] encryptedData, byte[] token) { + return tokenUnseal(encryptedData, token, null); + } +} diff --git a/java8/src/main/java/dev/o1c/spi/ByteOps.java b/java8/src/main/java/dev/o1c/spi/ByteOps.java index a1a3711..da6cd25 100644 --- a/java8/src/main/java/dev/o1c/spi/ByteOps.java +++ b/java8/src/main/java/dev/o1c/spi/ByteOps.java @@ -38,6 +38,10 @@ public static byte[] reverseCopyOf(byte[] buf) { return copy; } + public static int unpackInt(byte[] buf, int off) { + return (buf[off] & 0xff) << 24 | (buf[off + 1] & 0xff) << 16 | (buf[off + 2] & 0xff) << 8 | buf[off + 3] & 0xff; + } + public static byte[] fromHex(CharSequence data) { return HEX_DECODER.decode(data); }