1
+ package com.teamtailor.modules.encryption
2
+
3
+ import android.security.keystore.KeyGenParameterSpec
4
+ import android.security.keystore.KeyProperties
5
+ import android.util.Base64
6
+ import java.security.KeyPairGenerator
7
+ import java.security.KeyStore
8
+ import java.security.SecureRandom
9
+ import javax.crypto.Cipher
10
+ import javax.crypto.spec.GCMParameterSpec
11
+ import javax.crypto.spec.SecretKeySpec
12
+
13
+ object CryptoUtils {
14
+ private const val KEY_ALIAS = " com.teamtailor.modules.encryption.cryptoutils2"
15
+ private const val ANDROID_KEYSTORE = " AndroidKeyStore"
16
+ private const val GCM_TAG_LENGTH = 128
17
+
18
+ private fun getOrCreateKeyPair (): KeyStore .PrivateKeyEntry {
19
+ val keyStore = KeyStore .getInstance(ANDROID_KEYSTORE )
20
+ keyStore.load(null )
21
+
22
+ if (! keyStore.containsAlias(KEY_ALIAS )) {
23
+ val spec = KeyGenParameterSpec .Builder (
24
+ KEY_ALIAS ,
25
+ KeyProperties .PURPOSE_ENCRYPT or KeyProperties .PURPOSE_DECRYPT
26
+ )
27
+ .setDigests(KeyProperties .DIGEST_SHA1 ) // Changed to SHA1 to match Ruby/iOS
28
+ .setEncryptionPaddings(KeyProperties .ENCRYPTION_PADDING_RSA_OAEP )
29
+ .setKeySize(2048 ) // Explicitly set key size to match other implementations
30
+ .build()
31
+
32
+ val generator = KeyPairGenerator .getInstance(
33
+ KeyProperties .KEY_ALGORITHM_RSA ,
34
+ ANDROID_KEYSTORE
35
+ )
36
+ generator.initialize(spec)
37
+ generator.generateKeyPair()
38
+ }
39
+
40
+ return keyStore.getEntry(KEY_ALIAS , null ) as KeyStore .PrivateKeyEntry
41
+ }
42
+
43
+ fun getPublicKey (): String {
44
+ val entry = getOrCreateKeyPair()
45
+ return Base64 .encodeToString(entry.certificate.publicKey.encoded, Base64 .NO_WRAP )
46
+ }
47
+
48
+ fun hybridDecrypt (encryptedKey : String , cipherText : String , nonce : String , tag : String ): String {
49
+ val aesKey = rsaDecrypt(encryptedKey)
50
+ val cipher = Cipher .getInstance(" AES/GCM/NoPadding" )
51
+
52
+ val nonceBytes = Base64 .decode(nonce, Base64 .NO_WRAP )
53
+ val cipherBytes = Base64 .decode(cipherText, Base64 .NO_WRAP )
54
+ val tagBytes = Base64 .decode(tag, Base64 .NO_WRAP )
55
+
56
+ val spec = GCMParameterSpec (GCM_TAG_LENGTH , nonceBytes)
57
+ val secretKey = SecretKeySpec (aesKey, " AES" ) // AES-256 key from RSA decryption
58
+
59
+ cipher.init (Cipher .DECRYPT_MODE , secretKey, spec)
60
+ val decryptedBytes = cipher.doFinal(cipherBytes + tagBytes)
61
+
62
+ return String (decryptedBytes, Charsets .UTF_8 )
63
+ }
64
+
65
+ fun rsaDecrypt (encryptedBase64 : String ): ByteArray {
66
+ val entry = getOrCreateKeyPair()
67
+ // Use RSA/ECB/OAEPWithSHA-1AndMGF1Padding to match Ruby's OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
68
+ val cipher = Cipher .getInstance(" RSA/ECB/OAEPWithSHA-1AndMGF1Padding" )
69
+ cipher.init (Cipher .DECRYPT_MODE , entry.privateKey)
70
+
71
+ val encryptedBytes = Base64 .decode(encryptedBase64, Base64 .NO_WRAP )
72
+ return cipher.doFinal(encryptedBytes)
73
+ }
74
+
75
+ fun deleteKeyPair () {
76
+ val keyStore = KeyStore .getInstance(ANDROID_KEYSTORE )
77
+ keyStore.load(null )
78
+ keyStore.deleteEntry(KEY_ALIAS )
79
+ }
80
+
81
+ fun testEncryption (message : String ): Boolean {
82
+ val entry = getOrCreateKeyPair()
83
+ val cipher = Cipher .getInstance(" RSA/ECB/OAEPWithSHA-1AndMGF1Padding" )
84
+
85
+ // Encrypt
86
+ cipher.init (Cipher .ENCRYPT_MODE , entry.certificate.publicKey)
87
+ val encrypted = cipher.doFinal(message.toByteArray())
88
+
89
+ // Decrypt
90
+ cipher.init (Cipher .DECRYPT_MODE , entry.privateKey)
91
+ val decrypted = cipher.doFinal(encrypted)
92
+
93
+ return message == String (decrypted)
94
+ }
95
+ }
0 commit comments