Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit 56f1bfb

Browse files
committed
Added OpenSSL compliant encryption functionality to ensure compatibility to crypto-js.
1 parent fea5e80 commit 56f1bfb

File tree

2 files changed

+130
-17
lines changed

2 files changed

+130
-17
lines changed

src/main/scala/org/codeoverflow/chatoverflow/configuration/CryptoUtil.scala

Lines changed: 129 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package org.codeoverflow.chatoverflow.configuration
22

33
import 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

68
import javax.crypto.spec.{IvParameterSpec, PBEKeySpec, SecretKeySpec}
79
import 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
*/
1418
object 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)

src/main/scala/org/codeoverflow/chatoverflow/ui/web/rest/config/ConfigController.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class ConfigController(implicit val swagger: Swagger) extends JsonServlet with C
3030

3131
// Check for password correctness first
3232
if (!chatOverflow.credentialsService.checkPasswordCorrectness(password.toCharArray)) {
33-
ResultMessage(success = false, "Unable to load. Wrong password!")
33+
ResultMessage(success = false, "Unable to login. Wrong password!")
3434
} else {
3535

3636
// Password is correct. Login if first call.

0 commit comments

Comments
 (0)