11package org .codeoverflow .chatoverflow .configuration
22
33import java .nio .charset .StandardCharsets
4- import java .security .{MessageDigest , SecureRandom }
4+ import java .security .{DigestException , MessageDigest , SecureRandom }
5+ import java .util
6+ import java .util .Base64
57
68import javax .crypto .spec .{IvParameterSpec , PBEKeySpec , SecretKeySpec }
79import javax .crypto .{Cipher , SecretKeyFactory }
8- import org .apache .commons .codec .binary .Base64
910
1011/**
1112 * Provides methods to de- and encrypt using the AES-CBC algorithm.
13+ *
1214 * This code is based on https://gist.github.com/twuni/5668121.
15+ * The SSL compliant / crypto-js compliant code is based on
16+ * https://stackoverflow.com/questions/41432896/cryptojs-aes-encryption-and-java-aes-decryption
1317 */
1418object CryptoUtil {
1519
@@ -25,13 +29,122 @@ object CryptoUtil {
2529 * @return an auth key based the password an an random array
2630 */
2731 def generateAuthKey (password : String ): String = {
28-
2932 val authBase = runSpecificRandom.mkString + password
3033
3134 val digest = MessageDigest .getInstance(" SHA-256" )
3235 digest.digest(authBase.getBytes(StandardCharsets .UTF_8 )).mkString
3336 }
3437
38+ /**
39+ * Decrypts the ciphertext SSL compatible and thus also compatible to crypto-js.
40+ * Can be combined with: CryptoJS.AES.encrypt(msg, key).toString();
41+ *
42+ * @param secret the secret, password, key, call it whatever you want
43+ * @param ciphertext the ciphertext. Don't forget the integrity check ("CHECK")
44+ * @return Some plaintext if the key was correct and the integrity check was happy
45+ */
46+ def decryptSSLcompliant (secret : String , ciphertext : String ): Option [String ] = {
47+ try {
48+ val cipherData = Base64 .getDecoder.decode(ciphertext)
49+ val saltData = util.Arrays .copyOfRange(cipherData, 8 , 16 )
50+
51+ val md5 = MessageDigest .getInstance(" MD5" )
52+ val keyAndIV = generateSSLcompliantKeyAndIV(32 , 16 , 1 , saltData, secret.getBytes(StandardCharsets .UTF_8 ), md5)
53+ val key = new SecretKeySpec (keyAndIV(0 ), " AES" )
54+ val iv = new IvParameterSpec (keyAndIV(1 ))
55+
56+
57+ val encrypted = util.Arrays .copyOfRange(cipherData, 16 , cipherData.length)
58+ val aesCBC = Cipher .getInstance(" AES/CBC/PKCS5Padding" )
59+ aesCBC.init(Cipher .DECRYPT_MODE , key, iv)
60+ val decryptedData = aesCBC.doFinal(encrypted)
61+ val decrString = new String (decryptedData, StandardCharsets .UTF_8 )
62+
63+ // Same stupid integrity check
64+ if (! decrString.startsWith(" CHECK" )) {
65+ None
66+ } else {
67+ Some (decrString.substring(5 ))
68+ }
69+ } catch {
70+ case _ : Exception => None
71+ }
72+ }
73+
74+ /**
75+ * This helping function is based on the OpenSSL-Implementation.
76+ * Please see: https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c
77+ */
78+ private def generateSSLcompliantKeyAndIV (keyLength : Int , ivLength : Int , iterations : Int , salt : Array [Byte ], password : Array [Byte ], md : MessageDigest ): Array [Array [Byte ]] = {
79+ val digestLength = md.getDigestLength
80+ val requiredLength = (keyLength + ivLength + digestLength - 1 ) / digestLength * digestLength
81+ val generatedData = new Array [Byte ](requiredLength)
82+ var generatedLength = 0
83+ try {
84+ md.reset()
85+
86+ // Repeat process until sufficient data has been generated
87+ while ( {
88+ generatedLength < keyLength + ivLength
89+ }) { // Digest data (last digest if available, password data, salt if available)
90+ if (generatedLength > 0 ) md.update(generatedData, generatedLength - digestLength, digestLength)
91+ md.update(password)
92+ if (salt != null ) md.update(salt, 0 , 8 )
93+ md.digest(generatedData, generatedLength, digestLength)
94+ // additional rounds
95+ var i = 1
96+ while ( {
97+ i < iterations
98+ }) {
99+ md.update(generatedData, generatedLength, digestLength)
100+ md.digest(generatedData, generatedLength, digestLength)
101+
102+ {
103+ i += 1
104+ i - 1
105+ }
106+ }
107+ generatedLength += digestLength
108+ }
109+ // Copy key and IV into separate byte arrays
110+ val result = new Array [Array [Byte ]](2 )
111+ result(0 ) = util.Arrays .copyOfRange(generatedData, 0 , keyLength)
112+ if (ivLength > 0 ) result(1 ) = util.Arrays .copyOfRange(generatedData, keyLength, keyLength + ivLength)
113+ result
114+ } catch {
115+ case e : DigestException =>
116+ throw new RuntimeException (e)
117+ } finally {
118+ // Clean out temporary data
119+ util.Arrays .fill(generatedData, 0 .toByte)
120+ }
121+ }
122+
123+ /**
124+ * Encrypts the plaintext SSL compatible and thus also compatible to crypto-js.
125+ * Can be combined with: CryptoJS.AES.decrypt(msg, key).toString(CryptoJS.enc.Utf8);
126+ *
127+ * @param secret the secret, password, key, call it whatever you want
128+ * @param plaintext the plaintext to encrypt. adds the integrity check ("CHECK") internally
129+ * @return an ciphertext ready to be send
130+ */
131+ def encryptSSLcompliant (secret : String , plaintext : String ): String = {
132+ val saltData = generateSalt
133+ val md5 = MessageDigest .getInstance(" MD5" )
134+ val keyAndIV = generateSSLcompliantKeyAndIV(32 , 16 , 1 , saltData, secret.getBytes(StandardCharsets .UTF_8 ), md5)
135+ val key = new SecretKeySpec (keyAndIV(0 ), " AES" )
136+ val iv = new IvParameterSpec (keyAndIV(1 ))
137+
138+ val decrypted = (" CHECK" + plaintext).getBytes(StandardCharsets .UTF_8 )
139+ val aesCBC = Cipher .getInstance(" AES/CBC/PKCS5Padding" )
140+ aesCBC.init(Cipher .ENCRYPT_MODE , key, iv)
141+ val encrypted = aesCBC.doFinal(decrypted)
142+
143+ val message = " Salted__" .getBytes(StandardCharsets .UTF_8 ) ++ saltData ++ encrypted
144+
145+ new String (Base64 .getEncoder.encode(message), StandardCharsets .UTF_8 )
146+ }
147+
35148 /**
36149 * Encrypts the provided plaintext using AES.
37150 *
@@ -54,28 +167,28 @@ object CryptoUtil {
54167
55168 // Use a string builder and Base64 to save the init vector and encrypted data
56169 val ciphertext = new StringBuilder
57- ciphertext.append(Base64 .encodeBase64String(iv))
170+ ciphertext.append(org.apache.commons.codec.binary. Base64 .encodeBase64String(iv))
58171 ciphertext.append(" :" )
59- ciphertext.append(Base64 .encodeBase64String(salt))
172+ ciphertext.append(org.apache.commons.codec.binary. Base64 .encodeBase64String(salt))
60173 ciphertext.append(" :" )
61- ciphertext.append(Base64 .encodeBase64String(encrypted))
174+ ciphertext.append(org.apache.commons.codec.binary. Base64 .encodeBase64String(encrypted))
62175 ciphertext.toString
63176 }
64177
65- private def generateIV : Array [Byte ] = {
66- val random = new SecureRandom ()
67- val iv = new Array [Byte ](16 )
68- random.nextBytes(iv)
69- iv
70- }
71-
72178 private def generateSalt : Array [Byte ] = {
73179 val random = new SecureRandom ()
74180 val salt = new Array [Byte ](8 )
75181 random.nextBytes(salt)
76182 salt
77183 }
78184
185+ private def generateIV : Array [Byte ] = {
186+ val random = new SecureRandom ()
187+ val iv = new Array [Byte ](16 )
188+ random.nextBytes(iv)
189+ iv
190+ }
191+
79192 private def generateKeyFromString (password : Array [Char ], salt : Array [Byte ]) = {
80193 val factory = SecretKeyFactory .getInstance(" PBKDF2WithHmacSHA256" )
81194 val spec = new PBEKeySpec (password, salt, 10000 , 128 )
@@ -94,15 +207,15 @@ object CryptoUtil {
94207
95208 // Retrieve the used init vector from the ciphertext
96209 val parts = ciphertext.split(" :" )
97- val iv = Base64 .decodeBase64(parts(0 ))
98- val salt = Base64 .decodeBase64(parts(1 ))
210+ val iv = org.apache.commons.codec.binary. Base64 .decodeBase64(parts(0 ))
211+ val salt = org.apache.commons.codec.binary. Base64 .decodeBase64(parts(1 ))
99212
100213 // Generate a key from the password
101214 val key = generateKeyFromString(password, salt)
102215
103216 try {
104217 // Decrypt the ciphertext using AES
105- val encrypted = Base64 .decodeBase64(parts(2 ))
218+ val encrypted = org.apache.commons.codec.binary. Base64 .decodeBase64(parts(2 ))
106219 val cipher = Cipher .getInstance(key.getAlgorithm + " /CBC/PKCS5Padding" )
107220 cipher.init(Cipher .DECRYPT_MODE , key, new IvParameterSpec (iv))
108221 val decrypted = cipher.doFinal(encrypted)
0 commit comments