Skip to content

Commit 3323631

Browse files
committed
Started work in end2end encryption test
1 parent f41d2aa commit 3323631

File tree

5 files changed

+194
-1
lines changed

5 files changed

+194
-1
lines changed

backend/src/main/kotlin/app/cliq/backend/utils/TokenGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class TokenGenerator(
2222

2323
fun generatePasswordResetToken(): String = generateToken(RESET_PASSWORD_TOKEN_LENGTH).uppercase(getDefault())
2424

25-
private fun generateToken(length: UShort): String {
25+
fun generateToken(length: UShort): String {
2626
val randomBytes = ByteArray(length.toInt())
2727
secureRandomGenerator.nextBytes(randomBytes)
2828

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package app.cliq.backend.end2end
2+
3+
import app.cliq.backend.constants.Features
4+
import app.cliq.backend.support.DatabaseCleanupService
5+
import org.junit.jupiter.api.AfterEach
6+
import org.junit.jupiter.api.BeforeAll
7+
import org.junit.jupiter.api.Tag
8+
import org.junit.jupiter.api.TestInstance
9+
import org.springframework.beans.factory.annotation.Autowired
10+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
11+
import org.springframework.boot.test.context.SpringBootTest
12+
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc
13+
import org.springframework.context.annotation.ComponentScan
14+
import org.springframework.test.context.ActiveProfiles
15+
16+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
17+
@ConfigurationPropertiesScan(basePackages = ["app.cliq.backend.support"])
18+
@ComponentScan(basePackages = ["app.cliq.backend.support"])
19+
@AutoConfigureMockMvc
20+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
21+
@ActiveProfiles(Features.TEST)
22+
@Tag("end2end")
23+
annotation class End2EndTest
24+
25+
@End2EndTest
26+
abstract class End2EndTester {
27+
@BeforeAll
28+
@AfterEach
29+
fun clearDatabase(
30+
@Autowired cleaner: DatabaseCleanupService,
31+
) {
32+
cleaner.truncate()
33+
}
34+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package app.cliq.backend.end2end.auth
2+
3+
import app.cliq.backend.constants.EXAMPLE_EMAIL
4+
import app.cliq.backend.constants.EXAMPLE_PASSWORD
5+
import app.cliq.backend.end2end.End2EndTest
6+
import app.cliq.backend.end2end.End2EndTester
7+
import app.cliq.backend.support.EncryptionHelper
8+
import app.cliq.backend.support.KeyAndHashHelper
9+
import app.cliq.backend.utils.TokenGenerator
10+
import org.junit.jupiter.api.Test
11+
import org.springframework.beans.factory.annotation.Autowired
12+
13+
@End2EndTest
14+
class RegistrationTest(
15+
@Autowired
16+
private val tokenGenerator: TokenGenerator,
17+
@Autowired
18+
private val keyAndHashHelper: KeyAndHashHelper,
19+
@Autowired
20+
private val encryptionHelper: EncryptionHelper,
21+
) : End2EndTester() {
22+
/**
23+
* This test should test the whole registration flow, including actions that are being done by the frontend.
24+
*/
25+
@Test
26+
fun `test registration with keys creation`() {
27+
val email = EXAMPLE_EMAIL
28+
val password = EXAMPLE_PASSWORD
29+
30+
// Generate salt
31+
val salt = tokenGenerator.generateToken(16U)
32+
33+
// Generate UMK
34+
val argon2Generator = keyAndHashHelper.buildArgon2BytesGenerator(salt)
35+
val umk = ByteArray(32)
36+
argon2Generator.generateBytes(password.toByteArray(), umk)
37+
38+
// Generate DeviceKeyPair
39+
val deviceKeyPair = keyAndHashHelper.generateX25519KeyPair()
40+
41+
// Generate DEK
42+
val dek = tokenGenerator.generateToken(32U).toByteArray()
43+
// dek would be saved to a private key store
44+
45+
// Encrypt DEK with UMK
46+
val encryptedDek = encryptionHelper.encryptDEKWithUMK(dek, umk)
47+
// umk should be "dropped" here, only salt should be saved for later use in login
48+
49+
// Encrypt DEK with DeviceKeyPair
50+
val encryptedDekWithDeviceKeyPair = encryptionHelper.encryptDEKWithDeviceKeyPair(dek, deviceKeyPair)
51+
52+
// Encrypt DEK with DeviceKeyPair
53+
println()
54+
}
55+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package app.cliq.backend.support
2+
3+
import org.springframework.boot.test.context.TestComponent
4+
import java.security.SecureRandom
5+
import javax.crypto.Cipher
6+
import javax.crypto.spec.GCMParameterSpec
7+
import javax.crypto.spec.SecretKeySpec
8+
9+
@TestComponent
10+
class EncryptionHelper {
11+
data class EncryptedDEK(
12+
val nonce: ByteArray,
13+
val ciphertext: ByteArray,
14+
)
15+
16+
fun encryptDEKWithUMK(
17+
dek: ByteArray,
18+
umk: ByteArray,
19+
): EncryptedDEK {
20+
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
21+
22+
val nonce = ByteArray(12)
23+
SecureRandom().nextBytes(nonce)
24+
25+
val keySpec = SecretKeySpec(umk, "AES")
26+
val gcmSpec = GCMParameterSpec(128, nonce)
27+
28+
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec)
29+
30+
// Optional but recommended: bind context
31+
cipher.updateAAD("DEK_V1".toByteArray())
32+
33+
val ciphertext = cipher.doFinal(dek)
34+
35+
return EncryptedDEK(
36+
nonce = nonce,
37+
ciphertext = ciphertext
38+
)
39+
}
40+
41+
fun encryptDEKWithDeviceKeyPair(
42+
dek: ByteArray,
43+
deviceKeyPair: Pair<ByteArray, ByteArray>,
44+
): EncryptedDEK = encryptDEKWithUMK(dek, deviceKeyPair.second)
45+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package app.cliq.backend.support
2+
3+
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
4+
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
5+
import org.bouncycastle.crypto.generators.X25519KeyPairGenerator
6+
import org.bouncycastle.crypto.params.Argon2Parameters
7+
import org.bouncycastle.crypto.params.X25519KeyGenerationParameters
8+
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters
9+
import org.bouncycastle.crypto.params.X25519PublicKeyParameters
10+
import org.springframework.boot.test.context.TestComponent
11+
import java.security.SecureRandom
12+
13+
@TestComponent
14+
class KeyAndHashHelper {
15+
fun buildArgon2BytesGenerator(salt: String): Argon2BytesGenerator {
16+
val builder = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
17+
.withVersion(Argon2Parameters.ARGON2_VERSION_13)
18+
.withMemoryAsKB(65_536) // 64MB
19+
.withSalt(salt.toByteArray())
20+
.withIterations(2)
21+
.withParallelism(1)
22+
val params = builder.build()
23+
24+
val generator = Argon2BytesGenerator()
25+
generator.init(params)
26+
27+
return generator
28+
}
29+
30+
/**
31+
* @return Pair(publicKey, privateKey)
32+
*/
33+
fun generateX25519KeyPair(): Pair<ByteArray, ByteArray> {
34+
val generator = this.getX25519KeyPairGenerator()
35+
36+
val keyPair: AsymmetricCipherKeyPair = generator.generateKeyPair()
37+
38+
val privateKeyParams = keyPair.private as X25519PrivateKeyParameters
39+
val publicKeyParams = keyPair.public as X25519PublicKeyParameters
40+
41+
val privateKey = ByteArray(X25519PrivateKeyParameters.KEY_SIZE)
42+
val publicKey = ByteArray(X25519PublicKeyParameters.KEY_SIZE)
43+
44+
privateKeyParams.encode(privateKey, 0)
45+
publicKeyParams.encode(publicKey, 0)
46+
47+
return publicKey to privateKey
48+
}
49+
50+
private fun getX25519KeyPairGenerator(): X25519KeyPairGenerator {
51+
val random = SecureRandom()
52+
val params = X25519KeyGenerationParameters(random)
53+
54+
val generator = X25519KeyPairGenerator()
55+
generator.init(params)
56+
57+
return generator
58+
}
59+
}

0 commit comments

Comments
 (0)