diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java b/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java new file mode 100644 index 00000000..57a44454 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java @@ -0,0 +1,68 @@ +package io.opentdf.platform.sdk; + +import javax.crypto.*; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; + +public class AesGcm { + private static final int GCM_NONCE_LENGTH = 12; // in bytes + private static final int GCM_TAG_LENGTH = 16; // in bytes + private static final String CIPHER_TRANSFORM = "AES/GCM/NoPadding"; + + private final SecretKey key; + + /** + *

Constructor for AesGcm.

+ * + * @param key secret key for encryption and decryption + */ + public AesGcm(byte[] key) { + if (key.length == 0) { + throw new IllegalArgumentException("Invalid key size for gcm encryption"); + } + this.key = new SecretKeySpec(key, "AES"); + } + + /** + *

encrypt.

+ * + * @param plaintext the plaintext to encrypt + * @return the encrypted text + */ + public byte[] encrypt(byte[] plaintext) throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { + Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM); + byte[] nonce = new byte[GCM_NONCE_LENGTH]; + SecureRandom.getInstanceStrong().nextBytes(nonce); + GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); + cipher.init(Cipher.ENCRYPT_MODE, key, spec); + + byte[] cipherText = cipher.doFinal(plaintext); + byte[] cipherTextWithNonce = new byte[nonce.length + cipherText.length]; + System.arraycopy(nonce, 0, cipherTextWithNonce, 0, nonce.length); + System.arraycopy(cipherText, 0, cipherTextWithNonce, nonce.length, cipherText.length); + return cipherTextWithNonce; + } + + /** + *

decrypt.

+ * + * @param cipherTextWithNonce the ciphertext with nonce to decrypt + * @return the decrypted text + */ + public byte[] decrypt(byte[] cipherTextWithNonce) throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { + Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM); + byte[] nonce = Arrays.copyOfRange(cipherTextWithNonce, 0, GCM_NONCE_LENGTH); + byte[] cipherText = Arrays.copyOfRange(cipherTextWithNonce, GCM_NONCE_LENGTH, cipherTextWithNonce.length); + GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); + cipher.init(Cipher.DECRYPT_MODE, key, spec); + return cipher.doFinal(cipherText); + } +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java b/sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java new file mode 100644 index 00000000..0e1fb591 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java @@ -0,0 +1,47 @@ +package io.opentdf.platform.sdk; + +import javax.crypto.Cipher; +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +public class AsymDecryption { + private PrivateKey privateKey; + private static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----"; + private static final String PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----"; + private static final String CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + + /** + *

Constructor for AsymDecryption.

+ * + * @param privateKeyInPem a Private Key in PEM format + */ + public AsymDecryption(String privateKeyInPem) throws Exception { + String privateKeyPEM = privateKeyInPem + .replace(PRIVATE_KEY_HEADER, "") + .replace(PRIVATE_KEY_FOOTER, "") + .replaceAll("\\s", ""); // remove whitespaces + + byte[] decoded = Base64.getDecoder().decode(privateKeyPEM); + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded); + KeyFactory kf = KeyFactory.getInstance("RSA"); + this.privateKey = kf.generatePrivate(spec); + } + + /** + *

decrypt.

+ * + * @param data the data to decrypt + * @return the decrypted data + */ + public byte[] decrypt(byte[] data) throws Exception { + if (this.privateKey == null) { + throw new Exception("Failed to decrypt, private key is empty"); + } + + Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM); + cipher.init(Cipher.DECRYPT_MODE, this.privateKey); + return cipher.doFinal(data); + } +} \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java b/sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java new file mode 100644 index 00000000..aa63bbfd --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java @@ -0,0 +1,59 @@ +package io.opentdf.platform.sdk; + +import javax.crypto.Cipher; +import java.security.*; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +public class AsymEncryption { + private PublicKey publicKey; + private static final String PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----"; + private static final String PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----"; + private static final String CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + + /** + *

Constructor for AsymEncryption.

+ * + * @param publicKeyInPem a Public Key in PEM format + */ + public AsymEncryption(String publicKeyInPem) throws Exception { + publicKeyInPem = publicKeyInPem + .replace(PUBLIC_KEY_HEADER, "") + .replace(PUBLIC_KEY_FOOTER, "") + .replaceAll("\\s", ""); + + byte[] decoded = Base64.getDecoder().decode(publicKeyInPem); + X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded); + KeyFactory kf = KeyFactory.getInstance("RSA"); + this.publicKey = kf.generatePublic(spec); + } + + /** + *

encrypt.

+ * + * @param data the data to encrypt + * @return the encrypted data + */ + public byte[] encrypt(byte[] data) throws Exception { + if (this.publicKey == null) { + throw new Exception("Failed to encrypt, public key is empty"); + } + + Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM); + cipher.init(Cipher.ENCRYPT_MODE, this.publicKey); + return cipher.doFinal(data); + } + + /** + *

publicKeyInPemFormat.

+ * @return the public key in PEM format + */ + public String publicKeyInPemFormat() throws Exception { + if (this.publicKey == null) { + throw new Exception("Failed to generate PEM formatted public key"); + } + + String publicKeyPem = Base64.getEncoder().encodeToString(this.publicKey.getEncoded()); + return PUBLIC_KEY_HEADER + '\n' + publicKeyPem + '\n' + PUBLIC_KEY_FOOTER + '\n'; + } +} \ No newline at end of file diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AesGcmTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AesGcmTest.java new file mode 100644 index 00000000..bdd48e33 --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AesGcmTest.java @@ -0,0 +1,45 @@ +package io.opentdf.platform.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import org.junit.jupiter.api.Test; + +class AesGcmTest { + + @Test + void encryptionAndDecryptionWithValidKey() throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { + byte[] key = "ThisIsASecretKey".getBytes(); + AesGcm aesGcm = new AesGcm(key); + byte[] plaintext = "Virtru, JavaSDK!".getBytes(); + + byte[] cipherText = aesGcm.encrypt(plaintext); + byte[] decryptedText = aesGcm.decrypt(cipherText); + + assertArrayEquals(plaintext, decryptedText); + } + + @Test + void decryptionWithModifiedCipherText() throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { + byte[] key = "ThisIsASecretKey".getBytes(); + AesGcm aesGcm = new AesGcm(key); + byte[] plaintext = "Virtru, JavaSDK!".getBytes(); + + byte[] cipherText = aesGcm.encrypt(plaintext); + cipherText[0] = (byte) (cipherText[0] ^ 0x1); // Modify the ciphertext + + assertThrows(BadPaddingException.class, () -> aesGcm.decrypt(cipherText)); + } + + @Test + void encryptionWithEmptyKey() { + byte[] key = new byte[0]; + + assertThrows(IllegalArgumentException.class, () -> new AesGcm(key)); + } +} \ No newline at end of file diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AsymDecryptionTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AsymDecryptionTest.java new file mode 100644 index 00000000..95871fbd --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AsymDecryptionTest.java @@ -0,0 +1,54 @@ +package io.opentdf.platform.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +class AsymDecryptionTest { + + @Test + void decryptionWithValidPrivateKey() throws Exception { + String privateKeyInPem = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu8piKYWnESnnx\n" + + "kZOCJo9FgTOuKRNncY/QqFYhitAUBtZ0TSq9JL7wvFxQNCiKwWTW7HUI72K5SXJl\n" + + "0p4axvOxOYmN2Ticegfl/8GIvNMQl6YMiNc9Xm9hxj77hAqEVtl/9cstnaFLMC5f\n" + + "Plk4J6UD8pmz5lvX1WWtrkMIMnjeap2ioUgNLhyXya25EMLlTDfHVLna2c79Mo8M\n" + + "aPfZpVIYEqHQJYYBdZGamHMaPuQWmQDFPVvzbsytAxjGSfjDZYHwH9sYrPT51bKq\n" + + "efMZgBKFUnSsiI0Tx0NbNAJEF+K8t6ZeFfxQUgLKhLmLQjB6cW9OvpPzkS8zErdp\n" + + "ICUbJrffAgMBAAECggEAQqf36rGW5M0jjSDUPQCIEglaMX9A/2bLTsr0li8XfKnm\n" + + "R8WnBQ3dGkgKPBzDXaq1yxWoudDLoqETTyxiRP2Ml/e+KyeaZDQykjVR/dFD8cx1\n" + + "3cy9hYpXkb9A+/+hKi8VC6YQ1b57V/RxlqRgxf6E5u4mFd8tGx0ZcoU00Qi5+LOw\n" + + "tfSKLPNg4OujraCmrq/bgNkNEiMwHK2AQTbr31uhygUiGTmdXVZu1FLqg7WPGZyZ\n" + + "x/0U83x0dx6A890OFaebw7I84/tmyJYqutgkK3BhsvIHj1sr34PNdbAMACY/+vqu\n" + + "r5H7+oCvILlo4pVQ760NBWxIvv6GQGIedH/18GuX4QKBgQDlXxMuyUnUPVXCZWgw\n" + + "ckvaTXYpQY8om9L4tPeQpQWFhT7pVByQhxIKcmwpAvz5ZhTZUo3ERfuYgheoM6Ay\n" + + "0AXeuSGagpYWT+rb2DusmDgZIn6wBaMdZX1tll4rNTPOb0rRpQasyJRd6Li9+C9G\n" + + "A8cRxDKZfiaLSXi2xT45fZuFCQKBgQDDQgugZVGdbIMWJd5KfB7VQbjbkmt+6Q5r\n" + + "pMY4NMXYCggG1DUuaNwC5Ptl2QE3Z98ZJnYhAvXW0IaD7OyjqOItA00Q4+v8nv/I\n" + + "Bu5hCtIFHx+xCQtWL8IApjTckmen8YmFNBSgQHsCBR7oR51orgHu9HsPcTh5Jq5V\n" + + "oBr67a43pwKBgQCIYTJvtCFgv6NZNaBwhdUSFNK4DxIGzDfxxvAYIfaZgDN62pct\n" + + "XBJfAc/LxsoRpB+rZAmE9TN2Z4uXaDLNY6DJ3/vZ+eExnQ0A8J3yroNUdo0rLf7h\n" + + "gLHGUgzl1flauhObeWrxm0WUXMZTtdit4Zsgti5702UplmLfEYJA/q1UuQKBgBbb\n" + + "jHDia4N6SH43QKaHkTR11SYfJeZdcgq351x9EQwRYI8sGG2uaNMN60Ao/zN1PXC8\n" + + "R+flaNIU5ypaeflOs+uBD2yCwgV4t4i7BvzlP2DKG/Olk2YrgRKCYn3PxcKrS+YE\n" + + "CsYXxk6eOtgGSi8O77sBc8aDApFsLcxoScBGQrbRAoGANZKkKXpe7kNn05Td5JMY\n" + + "QchqRD9x1z/8zyWSnwpRlfUG3RnRRqyv14gHK1sTaIcb6hTKkcUnoP6cBQZOR8wQ\n" + + "b9jhi/YwGgMbC7wI11l70vJzf1ydKfI0JeGy51UP/8bCw8sAf67z/S7nwWZBUsrM\n" + + "X3wz+2TeAgQK0jsLzrv0R8A=\n" + + "-----END PRIVATE KEY-----\n"; + AsymDecryption asymDecryption = new AsymDecryption(privateKeyInPem); + String plaintext = "Virtru, JavaSDK!"; + + String str = "E/eLD2RiC+79RRyn8BqECVcvLThRGwmVQaaMffMf735n4VY40AX0DRINKj6DulI8KApM/R8J4BZLgGEsPMbkVdoPFSCZqaSnoINjNFXqIpAWOHG4MSyr2PPAy/H96DoXeKf1OlcE5LNWj8xY7Z25pxQjclmNyahQDSa7DrMkMBj8q5yDrB4uw239jy43CTQByNSHKDNpS5vMT1GXvGCQ9o44VKUtd16Yf0aI3HN5YoUqPHmgd4KwwO3ok0eiSc+TUopNTUgDyZB8oCojLnGm7F/JZIHPas/46D1cvSPt7XMuqnO2wzfSJiJcbsLFg0zBZf2GnVLQKhCPnDAf9AnobQ=="; + byte[] cipherText = Base64.getDecoder().decode(str); + + byte[] decryptedData = asymDecryption.decrypt(cipherText); + String decryptedStr = new String(decryptedData, StandardCharsets.UTF_8); + + assertNotNull(decryptedData); + assertEquals(plaintext, decryptedStr); + } +} \ No newline at end of file diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AsymEncryptionTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AsymEncryptionTest.java new file mode 100644 index 00000000..03e8a45c --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AsymEncryptionTest.java @@ -0,0 +1,46 @@ +package io.opentdf.platform.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import javax.crypto.Cipher; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import org.junit.jupiter.api.Test; + +class AsymEncryptionTest { + + @Test + void encryptionWithValidPublicKey() throws Exception { + String publicKeyInPem = "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArvKYimFpxEp58ZGTgiaP\n" + + "RYEzrikTZ3GP0KhWIYrQFAbWdE0qvSS+8LxcUDQoisFk1ux1CO9iuUlyZdKeGsbz\n" + + "sTmJjdk4nHoH5f/BiLzTEJemDIjXPV5vYcY++4QKhFbZf/XLLZ2hSzAuXz5ZOCel\n" + + "A/KZs+Zb19Vlra5DCDJ43mqdoqFIDS4cl8mtuRDC5Uw3x1S52tnO/TKPDGj32aVS\n" + + "GBKh0CWGAXWRmphzGj7kFpkAxT1b827MrQMYxkn4w2WB8B/bGKz0+dWyqnnzGYAS\n" + + "hVJ0rIiNE8dDWzQCRBfivLemXhX8UFICyoS5i0IwenFvTr6T85EvMxK3aSAlGya3\n" + + "3wIDAQAB\n" + + "-----END PUBLIC KEY-----";; + AsymEncryption asymEncryption = new AsymEncryption(publicKeyInPem); + byte[] plaintext = "Virtru, JavaSDK!".getBytes(); + + byte[] cipherText = asymEncryption.encrypt(plaintext); + + assertNotNull(cipherText); + } + + @Test + void encryptionWithInvalidPublicKey() { + String publicKeyInPem = "InvalidPublicKey"; + + assertThrows(Exception.class, () -> new AsymEncryption(publicKeyInPem)); + } + + @Test + void encryptionWithEmptyPublicKey() { + String publicKeyInPem = ""; + + assertThrows(Exception.class, () -> new AsymEncryption(publicKeyInPem)); + } +} \ No newline at end of file