Skip to content

Commit 6135833

Browse files
committed
Refactor encrypt/decrypt to support arbitrary transformation
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.
1 parent 704ece3 commit 6135833

File tree

2 files changed

+85
-99
lines changed

2 files changed

+85
-99
lines changed

src/main/scala/com/datasonnet/DS.scala

+69-82
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package com.datasonnet
1818

1919
import java.math.{BigDecimal, RoundingMode}
2020
import java.net.URL
21-
import java.nio.charset.{Charset, StandardCharsets}
21+
import java.security.SecureRandom
2222
import java.text.DecimalFormat
2323
import java.time.format.DateTimeFormatter
2424
import java.time.temporal.ChronoUnit
@@ -1066,68 +1066,61 @@ object DSLowercase extends Library {
10661066
},
10671067

10681068
/**
1069-
* Encrypts the value with specified algorithm, mode, and padding using the provided secret. Converts the encryption to a readable format with Base64
1070-
* Possible algorithms to use are AES, DES, and DESede. The provided secret must be of lengths 16 or 32, 8, and 24 respectively.
1071-
* All Algorithms only support NoPadding or PKCS5Padding
1069+
* Encrypts the value with specified JDK Cipher Transformation using the provided secret. Converts the encryption
1070+
* to a readable format with Base64
10721071
*
10731072
* @builtinParam value The message to be encrypted.
10741073
* @types [String]
10751074
* @builtinParam secret The secret used to encrypt the original messsage.
10761075
* @types [String]
1077-
* @builtinParam algorithm The algorithm used for the encryption.
1078-
* @types [String]
1079-
* @builtinParam mode The encryption mode to be used.
1080-
* @types [String]
1081-
* @builtinParam padding The encryption secret padding to be used
1076+
* @builtinParam transformation The string that describes the operation (or set of operations) to be performed on
1077+
* the given input, to produce some output. A transformation always includes the name of a cryptographic algorithm
1078+
* (e.g., AES), and may be followed by a feedback mode and padding scheme. A transformation is of the form:
1079+
* "algorithm/mode/padding" or "algorithm"
10821080
* @types [String]
1083-
*
10841081
* @builtinReturn Base64 String value of the encrypted message
10851082
* @types [String]
1086-
*
1087-
* @changed 0.7.1
1083+
* @changed 0.7.2
10881084
*/
1089-
builtin0("encrypt", "value", "secret", "algorithm", "mode", "padding") {
1090-
(vals, ev,fs) =>
1091-
val valSeq = validate(vals, ev, fs, Array(StringRead, StringRead, StringRead, StringRead, StringRead))
1085+
builtin0[Val]("encrypt", "value", "secret", "transformation") {
1086+
(vals, ev, fs) =>
1087+
val valSeq = validate(vals, ev, fs, Array(StringRead, StringRead, StringRead))
10921088
val value = valSeq(0).asInstanceOf[String]
10931089
val secret = valSeq(1).asInstanceOf[String]
1094-
val algorithm = valSeq(2).asInstanceOf[String]
1095-
val mode = valSeq(3).asInstanceOf[String]
1096-
val padding = valSeq(4).asInstanceOf[String]
1097-
var ivSize: Int = 0
1090+
val transformation = valSeq(2).asInstanceOf[String]
10981091

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

1103-
algorithm.toUpperCase() match {
1104-
case "AES" =>
1105-
ivSize = 16;
1106-
if(secret.length != 16 && secret.length != 32)
1107-
{throw Error.Delegate("Secret length must be 16 or 32 bytes, got: " + secret.length)}
1108-
case "DES" =>
1109-
ivSize = 8
1110-
if(secret.length != 8)
1111-
{throw Error.Delegate("Secret length must be 8 bytes, got: " + secret.length) }
1112-
case "DESEDE" =>
1113-
ivSize = 8
1114-
if(secret.length != 24)
1115-
{throw Error.Delegate("Secret length must be 24 bytes, got: " + secret.length) }
1116-
case i => throw Error.Delegate("Expected algorithm to be one of AES, DES, DESede, or RSA. Got: " + i)
1117-
}
1095+
// special case for ECB because of java.security.InvalidAlgorithmParameterException: ECB mode cannot use IV
1096+
if (transformTokens.length >= 2 && "ECB".equals(transformTokens(1))) {
1097+
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase))
1098+
Val.Str(Base64.getEncoder.encodeToString(cipher.doFinal(value.getBytes)))
11181099

1119-
var cipher: Cipher = null
1100+
} else {
1101+
// https://stackoverflow.com/a/52571774/4814697
1102+
val rand: SecureRandom = new SecureRandom()
1103+
val iv = new Array[Byte](cipher.getBlockSize)
1104+
rand.nextBytes(iv)
1105+
1106+
cipher.init(Cipher.ENCRYPT_MODE,
1107+
new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase),
1108+
new IvParameterSpec(iv),
1109+
rand)
1110+
1111+
// encrypted data:
1112+
val encryptedBytes = cipher.doFinal(value.getBytes)
1113+
1114+
// append Initiation Vector as a prefix to use it during decryption:
1115+
val combinedPayload = new Array[Byte](iv.length + encryptedBytes.length)
11201116

1121-
mode.toUpperCase match {
1122-
case "CBC" =>
1123-
cipher = Cipher.getInstance(algorithm.toUpperCase + "/CBC/" + padding)
1124-
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes, algorithm.toUpperCase), new IvParameterSpec(new Array[Byte](ivSize)))
1125-
case "ECB" =>
1126-
cipher = Cipher.getInstance(algorithm.toUpperCase + "/ECB/" + padding)
1127-
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes, algorithm.toUpperCase))
1128-
case i => throw Error.Delegate("Expected mode to be either CBC or ECB, got: " + i)
1117+
// populate payload with prefix IV and encrypted data
1118+
System.arraycopy(iv, 0, combinedPayload, 0, iv.length)
1119+
System.arraycopy(encryptedBytes, 0, combinedPayload, iv.length, encryptedBytes.length)
1120+
1121+
Val.Str(Base64.getEncoder.encodeToString(combinedPayload))
11291122
}
1130-
Val.Lazy(Val.Str(Base64.getEncoder.encodeToString(cipher.doFinal(value.getBytes)))).force
1123+
11311124
},
11321125

11331126
/**
@@ -1151,49 +1144,43 @@ object DSLowercase extends Library {
11511144
*
11521145
* @changed 0.7.1
11531146
*/
1154-
builtin0("decrypt", "value", "secret", "algorithm", "mode", "padding") {
1147+
builtin0[Val]("decrypt", "value", "secret", "transformation") {
11551148
(vals, ev,fs) =>
1156-
val valSeq = validate(vals, ev, fs, Array(StringRead, StringRead, StringRead, StringRead, StringRead))
1149+
val valSeq = validate(vals, ev, fs, Array(StringRead, StringRead, StringRead))
11571150
val value = valSeq(0).asInstanceOf[String]
11581151
val secret = valSeq(1).asInstanceOf[String]
1159-
val algorithm = valSeq(2).asInstanceOf[String]
1160-
val mode = valSeq(3).asInstanceOf[String]
1161-
val padding = valSeq(4).asInstanceOf[String]
1162-
var ivSize: Int = 0
1152+
val transformation = valSeq(2).asInstanceOf[String]
11631153

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

1168-
algorithm.toUpperCase() match {
1169-
case "AES" =>
1170-
ivSize = 16;
1171-
if(secret.length != 16 && secret.length != 32)
1172-
{throw Error.Delegate("Secret length must be 16 or 32 bytes, got: " + secret.length)}
1173-
case "DES" =>
1174-
ivSize = 8
1175-
if(secret.length != 8)
1176-
{throw Error.Delegate("Secret length must be 8 bytes, got: " + secret.length) }
1177-
case "DESEDE" =>
1178-
ivSize = 8
1179-
if(secret.length != 24)
1180-
{throw Error.Delegate("Secret length must be 24 bytes, got: " + secret.length) }
1181-
case i => throw Error.Delegate("Expected algorithm to be one of AES, DES, DESede, or RSA. Got: " + i)
1182-
}
1157+
// special case for ECB because of java.security.InvalidAlgorithmParameterException: ECB mode cannot use IV
1158+
if (transformTokens.length >= 2 && "ECB".equals(transformTokens(1))) {
1159+
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase))
1160+
Val.Str(new String(cipher.doFinal(Base64.getDecoder.decode(value))))
1161+
1162+
} else {
1163+
// https://stackoverflow.com/a/52571774/4814697
1164+
// separate prefix with IV from the rest of encrypted data//separate prefix with IV from the rest of encrypted data
1165+
val encryptedPayload = Base64.getDecoder.decode(value)
1166+
val iv = new Array[Byte](cipher.getBlockSize)
1167+
val encryptedBytes = new Array[Byte](encryptedPayload.length - iv.length)
1168+
val rand: SecureRandom = new SecureRandom()
1169+
1170+
// populate iv with bytes:
1171+
System.arraycopy(encryptedPayload, 0, iv, 0, iv.length)
11831172

1184-
var cipher: Cipher = null
1173+
// populate encryptedBytes with bytes:
1174+
System.arraycopy(encryptedPayload, iv.length, encryptedBytes, 0, encryptedBytes.length)
11851175

1186-
mode.toUpperCase match {
1187-
case "CBC" =>
1188-
cipher = Cipher.getInstance(algorithm.toUpperCase + "/CBC/" + padding)
1189-
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret.getBytes, algorithm.toUpperCase), new IvParameterSpec(new Array[Byte](ivSize)))
1190-
case "ECB" =>
1191-
cipher = Cipher.getInstance(algorithm.toUpperCase + "/ECB/" + padding)
1192-
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret.getBytes, algorithm.toUpperCase))
1193-
case i => throw Error.Delegate("Expected mode to be either CBC or ECB, got: " + i)
1176+
cipher.init(Cipher.DECRYPT_MODE,
1177+
new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase),
1178+
new IvParameterSpec(iv),
1179+
rand)
1180+
1181+
Val.Str(new String(cipher.doFinal(encryptedBytes)))
11941182
}
1195-
Val.Lazy(Val.Str(new String(cipher.doFinal(Base64.getDecoder.decode(value)), Charset.forName("UTF-8")))).force
1196-
},
1183+
}
11971184
),
11981185

11991186
"jsonpath" -> moduleFrom(

src/test/java/com/datasonnet/CryptoTest.java

+16-17
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
* limitations under the License.
1717
*/
1818

19-
import org.junit.jupiter.api.Disabled;
2019
import org.junit.jupiter.api.Test;
2120

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

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

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

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

104103
//=============================ECB
105104

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

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

114113
//========================================================================================
115114

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

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

125124
//=============================ECB
126125

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

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

135134
//========================================================================================
136135

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

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

146145
//=============================ECB
147146

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

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

@@ -158,10 +157,10 @@ void testEncryptDecrypt() {
158157
/*alg ="RSA";
159158
mode="ECB";
160159
padding="PKCS1Padding";
161-
mapper = new Mapper("ds.crypto.encrypt(\"Hello World\", \"DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
160+
mapper = new Mapper("ds.crypto.encrypt('Hello World', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
162161
encrypted = mapper.transform("{}").replaceAll("\"", "");
163162
164-
mapper = new Mapper("ds.crypto.decrypt(\"" + encrypted + "\", \"DataSonnet123456\", \"" + alg + "\", \"" + mode + "\", \"" + padding + "\")");
163+
mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')");
165164
decrypted = mapper.transform("{}").replaceAll("\"", "");
166165
assertEquals("Hello World", decrypted);*/
167166

0 commit comments

Comments
 (0)