From 9a5aaf5a8c582cfd97386ed50cf67850e679dba4 Mon Sep 17 00:00:00 2001 From: javaduke Date: Wed, 26 Apr 2023 16:44:06 -0600 Subject: [PATCH] Crypto enhancement (#119) * Added IV as a parameter for encrypt and decrypt * Vulnerabilities fixes --- docs/modules/ROOT/pages/libraries-crypto.adoc | 14 +++-- pom.xml | 11 ++-- src/main/scala/com/datasonnet/DS.scala | 57 ++++++++++++------- src/test/java/com/datasonnet/CryptoTest.java | 16 +++++- 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/docs/modules/ROOT/pages/libraries-crypto.adoc b/docs/modules/ROOT/pages/libraries-crypto.adoc index cf744716..c8618c80 100644 --- a/docs/modules/ROOT/pages/libraries-crypto.adoc +++ b/docs/modules/ROOT/pages/libraries-crypto.adoc @@ -1,33 +1,35 @@ ## crypto -### `decrypt(string value, string secret, string algorithm, string mode, string padding)` +### `decrypt(string value, string secret, string transformation, string iv = null)` Decrypts the Base64 value with specified JDK Cipher Transformation string and the provided secret. The transformation string 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". See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/crypto/Cipher.html[Java Cipher] for more information. +The optional IV parameter sets the initialization vector, if null or not specified, a random one will be generated. *Example:* ------------------------ -ds.crypto.decrypt("Hello World", "DataSonnet123456", "AES/ECB/PKCS5Padding") +ds.crypto.decrypt("HrkF1grBXCtATMLxh1gZVA==", "DataSonnet123456", "AES/ECB/PKCS5Padding") ------------------------ .Result ------------------------ -"HrkF1grBXCtATMLxh1gZVA==" +"Hello World" ------------------------ -### `encrypt(string value, string secret, string transformation)` +### `encrypt(string value, string secret, string transformation, string iv = null)` Encrypts the value with specified JDK Cipher Transformation and the provided secret. Converts the encryption to a readable format with Base64. The transformation string 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". See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/crypto/Cipher.html[Java Cipher] for more information. +The optional IV parameter sets the initialization vector, if null or not specified, a random one will be generated. *Example:* ------------------------ -ds.crypto.decrypt("HrkF1grBXCtATMLxh1gZVA==", "DataSonnet123456", "AES/ECB/PKCS5Padding") +ds.crypto.encrypt("Hello World", "DataSonnet123456", "AES/ECB/PKCS5Padding") ------------------------ .Result ------------------------ -"Hello World" +"HrkF1grBXCtATMLxh1gZVA==" ------------------------ ### `hash(string value, string algorithm)` diff --git a/pom.xml b/pom.xml index fb68346d..3f907c60 100644 --- a/pom.xml +++ b/pom.xml @@ -70,12 +70,12 @@ UTF-8 UTF-8 - 2.14.0 + 2.15.0 5.6.2 - 1.68 + 1.69 0.9.4 2.7.0 - 2.7.0 + 2.8.0 4.1 @@ -292,6 +292,7 @@ test + @@ -608,7 +609,7 @@ - + diff --git a/src/main/scala/com/datasonnet/DS.scala b/src/main/scala/com/datasonnet/DS.scala index bcb1f37a..3a48133f 100644 --- a/src/main/scala/com/datasonnet/DS.scala +++ b/src/main/scala/com/datasonnet/DS.scala @@ -1181,16 +1181,17 @@ object DSLowercase extends Library { * (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] + * @builtinParam iv Optional initialization vector. + * @types [String] * @builtinReturn Base64 String value of the encrypted message * @types [String] * @changed 2.0.3 */ - 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 transformation = valSeq(2).asInstanceOf[String] + builtinWithDefaults[Val]("encrypt", "value" -> None, "secret" -> None, "transformation" -> None, "iv" -> Some(Expr.Null(0))) { + (args, ev) => + val value = args("value").asInstanceOf[Val.Str].value + val secret = args("secret").asInstanceOf[Val.Str].value + val transformation = args("transformation").asInstanceOf[Val.Str].value val cipher = Cipher.getInstance(transformation) val transformTokens = transformation.split("/") @@ -1203,8 +1204,14 @@ object DSLowercase extends Library { } else { // https://stackoverflow.com/a/52571774/4814697 val rand: SecureRandom = new SecureRandom() - val iv = new Array[Byte](cipher.getBlockSize) - rand.nextBytes(iv) + + val iv: Array[Byte] = if (args("iv") == Val.Null) { + val newIV = new Array[Byte](cipher.getBlockSize) + rand.nextBytes(newIV) + newIV + } else { + args("iv").asInstanceOf[Val.Str].value.getBytes + } cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes, transformTokens(0).toUpperCase), @@ -1233,22 +1240,22 @@ object DSLowercase extends Library { * @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. + * @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] - * @builtinParam padding The encryption secret padding to be used + * @builtinParam iv Optional initialization vector. * @types [String] - * @builtinReturn Base64 String value of the encrypted message + * @builtinReturn String value of the decrypted message * @types [String] * @changed 2.0.3 */ - builtin0[Val]("decrypt", "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 transformation = valSeq(2).asInstanceOf[String] + builtinWithDefaults[Val]("decrypt", "value" -> None, "secret" -> None, "transformation" -> None, "iv" -> Some(Expr.Null(0))) { + (args, ev) => + val value = args("value").asInstanceOf[Val.Str].value + val secret = args("secret").asInstanceOf[Val.Str].value + val transformation = args("transformation").asInstanceOf[Val.Str].value val cipher = Cipher.getInstance(transformation) val transformTokens = transformation.split("/") @@ -1262,10 +1269,18 @@ object DSLowercase extends Library { // 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() + val iv: Array[Byte] = if (args("iv") == Val.Null) { + val newIV = new Array[Byte](cipher.getBlockSize) + rand.nextBytes(newIV) + newIV + } else { + args("iv").asInstanceOf[Val.Str].value.getBytes + } + + val encryptedBytes = new Array[Byte](encryptedPayload.length - iv.length) + // populate iv with bytes: System.arraycopy(encryptedPayload, 0, iv, 0, iv.length) diff --git a/src/test/java/com/datasonnet/CryptoTest.java b/src/test/java/com/datasonnet/CryptoTest.java index 505291d6..7f47f7a4 100644 --- a/src/test/java/com/datasonnet/CryptoTest.java +++ b/src/test/java/com/datasonnet/CryptoTest.java @@ -1,7 +1,7 @@ package com.datasonnet; /*- - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -185,5 +184,18 @@ void testEncryptDecrypt() { String msg = e.getMessage(); assertTrue(msg != null && msg.contains("Caused by: java.security.InvalidKeyException: Invalid AES key length")); } + + //Encrypt / decrypt with provided IV + alg ="AES"; mode="CBC"; padding="PKCS5Padding"; + mapper = new Mapper("ds.crypto.encrypt('Hello World', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "', 's9X([cZ{#W{(x6Y7')"); + encrypted = mapper.transform("{}").replaceAll("\"", ""); + + mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "', 's9X([cZ{#W{(x6Y7')"); + decrypted = mapper.transform("{}").replaceAll("\"", ""); + assertEquals("Hello World", decrypted); + + mapper = new Mapper("ds.crypto.decrypt('" + encrypted + "', 'DataSonnet123456', '" + alg + "/" + mode + "/" + padding + "')"); + decrypted = mapper.transform("{}").replaceAll("\"", ""); + assertEquals("Hello World", decrypted); } }