Skip to content

Commit

Permalink
Refactor encrypt/decrypt to support arbitrary transformation
Browse files Browse the repository at this point in the history
In order to support any arbitrary transformation, with or without mode
and padding, the functions now parse the transformation string and apply
the necessary init vector. This also delegates validation to each
cipher.
  • Loading branch information
jam01 committed Dec 2, 2020
1 parent 704ece3 commit 6135833
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 99 deletions.
151 changes: 69 additions & 82 deletions src/main/scala/com/datasonnet/DS.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package com.datasonnet

import java.math.{BigDecimal, RoundingMode}
import java.net.URL
import java.nio.charset.{Charset, StandardCharsets}
import java.security.SecureRandom
import java.text.DecimalFormat
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
Expand Down Expand Up @@ -1066,68 +1066,61 @@ object DSLowercase extends Library {
},

/**
* Encrypts the value with specified algorithm, mode, and padding using the provided secret. Converts the encryption to a readable format with Base64
* Possible algorithms to use are AES, DES, and DESede. The provided secret must be of lengths 16 or 32, 8, and 24 respectively.
* All Algorithms only support NoPadding or PKCS5Padding
* Encrypts the value with specified JDK Cipher Transformation using the provided secret. Converts the encryption
* to a readable format with Base64
*
* @builtinParam value The message to be encrypted.
* @types [String]
* @builtinParam secret The secret used to encrypt the original messsage.
* @types [String]
* @builtinParam algorithm The algorithm used for the encryption.
* @types [String]
* @builtinParam mode The encryption mode to be used.
* @types [String]
* @builtinParam padding The encryption secret padding to be used
* @builtinParam transformation The string that describes the operation (or set of operations) to be performed on
* the given input, to produce some output. A transformation always includes the name of a cryptographic algorithm
* (e.g., AES), and may be followed by a feedback mode and padding scheme. A transformation is of the form:
* "algorithm/mode/padding" or "algorithm"
* @types [String]
*
* @builtinReturn Base64 String value of the encrypted message
* @types [String]
*
* @changed 0.7.1
* @changed 0.7.2
*/
builtin0("encrypt", "value", "secret", "algorithm", "mode", "padding") {
(vals, ev,fs) =>
val valSeq = validate(vals, ev, fs, Array(StringRead, StringRead, StringRead, StringRead, StringRead))
builtin0[Val]("encrypt", "value", "secret", "transformation") {
(vals, ev, fs) =>
val valSeq = validate(vals, ev, fs, Array(StringRead, StringRead, StringRead))
val value = valSeq(0).asInstanceOf[String]
val secret = valSeq(1).asInstanceOf[String]
val algorithm = valSeq(2).asInstanceOf[String]
val mode = valSeq(3).asInstanceOf[String]
val padding = valSeq(4).asInstanceOf[String]
var ivSize: Int = 0
val transformation = valSeq(2).asInstanceOf[String]

if(!padding.equalsIgnoreCase("NoPadding") && !padding.equalsIgnoreCase("PKCS5Padding")){
{throw Error.Delegate("Padding must be either: NoPadding or PKCS5Padding, got: " + padding)}
}
val cipher = Cipher.getInstance(transformation)
val transformTokens = transformation.split("/")

algorithm.toUpperCase() match {
case "AES" =>
ivSize = 16;
if(secret.length != 16 && secret.length != 32)
{throw Error.Delegate("Secret length must be 16 or 32 bytes, got: " + secret.length)}
case "DES" =>
ivSize = 8
if(secret.length != 8)
{throw Error.Delegate("Secret length must be 8 bytes, got: " + secret.length) }
case "DESEDE" =>
ivSize = 8
if(secret.length != 24)
{throw Error.Delegate("Secret length must be 24 bytes, got: " + secret.length) }
case i => throw Error.Delegate("Expected algorithm to be one of AES, DES, DESede, or RSA. Got: " + i)
}
// special case for ECB because of java.security.InvalidAlgorithmParameterException: ECB mode cannot use IV
if (transformTokens.length >= 2 && "ECB".equals(transformTokens(1))) {
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase))
Val.Str(Base64.getEncoder.encodeToString(cipher.doFinal(value.getBytes)))

var cipher: Cipher = null
} else {
// https://stackoverflow.com/a/52571774/4814697
val rand: SecureRandom = new SecureRandom()
val iv = new Array[Byte](cipher.getBlockSize)
rand.nextBytes(iv)

cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase),
new IvParameterSpec(iv),
rand)

// encrypted data:
val encryptedBytes = cipher.doFinal(value.getBytes)

// append Initiation Vector as a prefix to use it during decryption:
val combinedPayload = new Array[Byte](iv.length + encryptedBytes.length)

mode.toUpperCase match {
case "CBC" =>
cipher = Cipher.getInstance(algorithm.toUpperCase + "/CBC/" + padding)
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes, algorithm.toUpperCase), new IvParameterSpec(new Array[Byte](ivSize)))
case "ECB" =>
cipher = Cipher.getInstance(algorithm.toUpperCase + "/ECB/" + padding)
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes, algorithm.toUpperCase))
case i => throw Error.Delegate("Expected mode to be either CBC or ECB, got: " + i)
// populate payload with prefix IV and encrypted data
System.arraycopy(iv, 0, combinedPayload, 0, iv.length)
System.arraycopy(encryptedBytes, 0, combinedPayload, iv.length, encryptedBytes.length)

Val.Str(Base64.getEncoder.encodeToString(combinedPayload))
}
Val.Lazy(Val.Str(Base64.getEncoder.encodeToString(cipher.doFinal(value.getBytes)))).force

},

/**
Expand All @@ -1151,49 +1144,43 @@ object DSLowercase extends Library {
*
* @changed 0.7.1
*/
builtin0("decrypt", "value", "secret", "algorithm", "mode", "padding") {
builtin0[Val]("decrypt", "value", "secret", "transformation") {
(vals, ev,fs) =>
val valSeq = validate(vals, ev, fs, Array(StringRead, StringRead, StringRead, StringRead, StringRead))
val valSeq = validate(vals, ev, fs, Array(StringRead, StringRead, StringRead))
val value = valSeq(0).asInstanceOf[String]
val secret = valSeq(1).asInstanceOf[String]
val algorithm = valSeq(2).asInstanceOf[String]
val mode = valSeq(3).asInstanceOf[String]
val padding = valSeq(4).asInstanceOf[String]
var ivSize: Int = 0
val transformation = valSeq(2).asInstanceOf[String]

if(!padding.equalsIgnoreCase("NoPadding") && !padding.equalsIgnoreCase("PKCS5Padding")){
{throw Error.Delegate("Padding must be either: NoPadding or PKCS5Padding, got: " + padding)}
}
val cipher = Cipher.getInstance(transformation)
val transformTokens = transformation.split("/")

algorithm.toUpperCase() match {
case "AES" =>
ivSize = 16;
if(secret.length != 16 && secret.length != 32)
{throw Error.Delegate("Secret length must be 16 or 32 bytes, got: " + secret.length)}
case "DES" =>
ivSize = 8
if(secret.length != 8)
{throw Error.Delegate("Secret length must be 8 bytes, got: " + secret.length) }
case "DESEDE" =>
ivSize = 8
if(secret.length != 24)
{throw Error.Delegate("Secret length must be 24 bytes, got: " + secret.length) }
case i => throw Error.Delegate("Expected algorithm to be one of AES, DES, DESede, or RSA. Got: " + i)
}
// special case for ECB because of java.security.InvalidAlgorithmParameterException: ECB mode cannot use IV
if (transformTokens.length >= 2 && "ECB".equals(transformTokens(1))) {
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase))
Val.Str(new String(cipher.doFinal(Base64.getDecoder.decode(value))))

} else {
// https://stackoverflow.com/a/52571774/4814697
// separate prefix with IV from the rest of encrypted data//separate prefix with IV from the rest of encrypted data
val encryptedPayload = Base64.getDecoder.decode(value)
val iv = new Array[Byte](cipher.getBlockSize)
val encryptedBytes = new Array[Byte](encryptedPayload.length - iv.length)
val rand: SecureRandom = new SecureRandom()

// populate iv with bytes:
System.arraycopy(encryptedPayload, 0, iv, 0, iv.length)

var cipher: Cipher = null
// populate encryptedBytes with bytes:
System.arraycopy(encryptedPayload, iv.length, encryptedBytes, 0, encryptedBytes.length)

mode.toUpperCase match {
case "CBC" =>
cipher = Cipher.getInstance(algorithm.toUpperCase + "/CBC/" + padding)
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret.getBytes, algorithm.toUpperCase), new IvParameterSpec(new Array[Byte](ivSize)))
case "ECB" =>
cipher = Cipher.getInstance(algorithm.toUpperCase + "/ECB/" + padding)
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret.getBytes, algorithm.toUpperCase))
case i => throw Error.Delegate("Expected mode to be either CBC or ECB, got: " + i)
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase),
new IvParameterSpec(iv),
rand)

Val.Str(new String(cipher.doFinal(encryptedBytes)))
}
Val.Lazy(Val.Str(new String(cipher.doFinal(Base64.getDecoder.decode(value)), Charset.forName("UTF-8")))).force
},
}
),

"jsonpath" -> moduleFrom(
Expand Down
33 changes: 16 additions & 17 deletions src/test/java/com/datasonnet/CryptoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
* limitations under the License.
*/

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
Expand Down Expand Up @@ -86,70 +85,70 @@ void testHMAC() {
@Test
void testEncryptDecrypt() {
String alg ="AES", mode="CBC", padding="PKCS5Padding";
Mapper mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
Mapper mapper = new Mapper("ds.crypto.encrypt('Hello World', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
String encrypted = mapper.transform("{}").replaceAll("\"", "");

mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
String decrypted = mapper.transform("{}").replaceAll("\"", "");
assertEquals("Hello World", decrypted);

// 32 bits long
mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"DataSonnet123456DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.encrypt('Hello World', 'DataSonnet123456DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
encrypted = mapper.transform("{}").replaceAll("\"", "");

mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"DataSonnet123456DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonnet123456DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
decrypted = mapper.transform("{}").replaceAll("\"", "");
assertEquals("Hello World", decrypted);

//=============================ECB

mode="ECB";
mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.encrypt('Hello World', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
encrypted = mapper.transform("{}").replaceAll("\"", "");

mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
decrypted = mapper.transform("{}").replaceAll("\"", "");
assertEquals("Hello World", decrypted);

//========================================================================================

alg ="DES";
mode="CBC";
mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"DataSonn\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.encrypt('Hello World', 'DataSonn', '" + alg + "/" + mode + "/" + padding + "')");
encrypted = mapper.transform("{}").replaceAll("\"", "");

mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"DataSonn\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonn', '" + alg + "/" + mode + "/" + padding + "')");
decrypted = mapper.transform("{}").replaceAll("\"", "");
assertEquals("Hello World", decrypted);

//=============================ECB

mode="ECB";
mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"DataSonn\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.encrypt('Hello World', 'DataSonn', '" + alg + "/" + mode + "/" + padding + "')");
encrypted = mapper.transform("{}").replaceAll("\"", "");

mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"DataSonn\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonn', '" + alg + "/" + mode + "/" + padding + "')");
decrypted = mapper.transform("{}").replaceAll("\"", "");
assertEquals("Hello World", decrypted);

//========================================================================================

alg ="DESede";
mode="CBC";
mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"Datasonnet123456XDatason\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.encrypt('Hello World', 'Datasonnet123456XDatason', '" + alg + "/" + mode + "/" + padding + "')");
encrypted = mapper.transform("{}").replaceAll("\"", "");

mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"Datasonnet123456XDatason\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'Datasonnet123456XDatason', '" + alg + "/" + mode + "/" + padding + "')");
decrypted = mapper.transform("{}").replaceAll("\"", "");
assertEquals("Hello World", decrypted);

//=============================ECB

mode="ECB";
mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"Datasonnet123456XDatason\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.encrypt('Hello World', 'Datasonnet123456XDatason', '" + alg + "/" + mode + "/" + padding + "')");
encrypted = mapper.transform("{}").replaceAll("\"", "");

mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"Datasonnet123456XDatason\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'Datasonnet123456XDatason', '" + alg + "/" + mode + "/" + padding + "')");
decrypted = mapper.transform("{}").replaceAll("\"", "");
assertEquals("Hello World", decrypted);

Expand All @@ -158,10 +157,10 @@ void testEncryptDecrypt() {
/*alg ="RSA";
mode="ECB";
padding="PKCS1Padding";
mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.encrypt('Hello World', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
encrypted = mapper.transform("{}").replaceAll("\"", "");
mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
decrypted = mapper.transform("{}").replaceAll("\"", "");
assertEquals("Hello World", decrypted);*/

Expand Down

0 comments on commit 6135833

Please sign in to comment.