-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This relates to #4.
- Loading branch information
Showing
7 changed files
with
390 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Oops, something went wrong.