feat: Simplify AES key encryption and decryption process in MessageCrypto by removing unnecessary conversions and enhancing logging
This commit is contained in:
@@ -186,10 +186,13 @@ fun encryptWithPassword(data: String, password: String): String
|
|||||||
**Алгоритм:**
|
**Алгоритм:**
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Сжатие данных (Deflate)
|
1. Сжатие данных (zlib Deflate)
|
||||||
data → compressed bytes
|
data → compressed bytes
|
||||||
|
(совместимо с pako.deflate в JS)
|
||||||
|
|
||||||
2. Деривация ключа (PBKDF2-HMAC-SHA256)
|
2. Деривация ключа (PBKDF2-HMAC-SHA1)
|
||||||
|
⚠️ ВАЖНО: SHA1, не SHA256!
|
||||||
|
(crypto-js по умолчанию использует SHA1)
|
||||||
password + "rosetta" salt + 1000 iterations
|
password + "rosetta" salt + 1000 iterations
|
||||||
→ 256-bit AES key
|
→ 256-bit AES key
|
||||||
|
|
||||||
@@ -205,6 +208,19 @@ fun encryptWithPassword(data: String, password: String): String
|
|||||||
"aGVsbG8=:d29ybGQ="
|
"aGVsbG8=:d29ybGQ="
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Кросс-платформенная совместимость:**
|
||||||
|
|
||||||
|
| Параметр | JS (crypto-js) | Kotlin |
|
||||||
|
| ----------- | -------------- | ----------------- |
|
||||||
|
| PBKDF2 | HMAC-SHA1 | HMAC-SHA1 ✅ |
|
||||||
|
| Salt | "rosetta" | "rosetta" ✅ |
|
||||||
|
| Iterations | 1000 | 1000 ✅ |
|
||||||
|
| Key size | 256 bit | 256 bit ✅ |
|
||||||
|
| Cipher | AES-256-CBC | AES-256-CBC ✅ |
|
||||||
|
| Padding | PKCS7 | PKCS5 (=PKCS7) ✅ |
|
||||||
|
| Compression | pako.deflate | Deflater ✅ |
|
||||||
|
| Format | iv:ct (base64) | iv:ct (base64) ✅ |
|
||||||
|
|
||||||
**Зачем сжатие?**
|
**Зачем сжатие?**
|
||||||
|
|
||||||
- Seed phrase (12 слов ≈ 100 байт) → ~50 байт после сжатия
|
- Seed phrase (12 слов ≈ 100 байт) → ~50 байт после сжатия
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package com.rosetta.messenger.crypto
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🧪 Тесты кросс-платформенной совместимости шифрования
|
||||||
|
*
|
||||||
|
* Проверяют совместимость между:
|
||||||
|
* - Kotlin (CryptoManager.kt)
|
||||||
|
* - JS/React Native (cryptoJSI.ts)
|
||||||
|
*
|
||||||
|
* Алгоритм:
|
||||||
|
* - PBKDF2-HMAC-SHA1 (1000 iterations, salt="rosetta")
|
||||||
|
* - AES-256-CBC + PKCS7 padding
|
||||||
|
* - zlib compression (deflate/inflate)
|
||||||
|
* - Format: base64(iv):base64(ciphertext)
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class CryptoCompatibilityTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
java.security.Security.addProvider(org.bouncycastle.jce.provider.BouncyCastleProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 🔐 Тестовые данные, зашифрованные на JS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Эти данные были зашифрованы на JS (cryptoJSI.ts):
|
||||||
|
*
|
||||||
|
* const encrypted = encodeWithPasswordJSI("testPassword123", "Hello World");
|
||||||
|
* console.log(encrypted);
|
||||||
|
*
|
||||||
|
* Параметры:
|
||||||
|
* - password: "testPassword123"
|
||||||
|
* - plaintext: "Hello World"
|
||||||
|
* - salt: "rosetta"
|
||||||
|
* - iterations: 1000
|
||||||
|
* - hash: SHA1
|
||||||
|
*/
|
||||||
|
companion object {
|
||||||
|
// ⚠️ TODO: Вставьте реальные зашифрованные данные из JS консоли
|
||||||
|
// Для генерации выполните в RN/JS:
|
||||||
|
//
|
||||||
|
// import { encodeWithPasswordJSI } from './cryptoJSI';
|
||||||
|
// console.log('Test 1:', encodeWithPasswordJSI("testPassword123", "Hello World"));
|
||||||
|
// console.log('Test 2:', encodeWithPasswordJSI("mySecretPass", '{"key":"value","number":42}'));
|
||||||
|
// console.log('Test 3:', encodeWithPasswordJSI("password", ""));
|
||||||
|
// console.log('Test 4:', encodeWithPasswordJSI("пароль123", "Привет мир! 🔐"));
|
||||||
|
|
||||||
|
// Placeholder - замените реальными данными из JS
|
||||||
|
const val JS_ENCRYPTED_HELLO_WORLD = "" // encodeWithPasswordJSI("testPassword123", "Hello World")
|
||||||
|
const val JS_ENCRYPTED_JSON = "" // encodeWithPasswordJSI("mySecretPass", '{"key":"value","number":42}')
|
||||||
|
const val JS_ENCRYPTED_EMPTY = "" // encodeWithPasswordJSI("password", "")
|
||||||
|
const val JS_ENCRYPTED_CYRILLIC = "" // encodeWithPasswordJSI("пароль123", "Привет мир! 🔐")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ✅ Тесты Kotlin → Kotlin (базовая проверка)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun kotlin_encrypt_decrypt_roundtrip() {
|
||||||
|
val originalData = "Hello, World! This is a secret message."
|
||||||
|
val password = "testPassword123"
|
||||||
|
|
||||||
|
val encrypted = CryptoManager.encryptWithPassword(originalData, password)
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
|
||||||
|
|
||||||
|
assertNotNull("Encrypted data should not be null", encrypted)
|
||||||
|
assertTrue("Encrypted should contain ':'", encrypted.contains(":"))
|
||||||
|
assertEquals("Decrypted should match original", originalData, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun kotlin_encrypt_decrypt_empty_string() {
|
||||||
|
val originalData = ""
|
||||||
|
val password = "password"
|
||||||
|
|
||||||
|
val encrypted = CryptoManager.encryptWithPassword(originalData, password)
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
|
||||||
|
|
||||||
|
assertEquals("Empty string should roundtrip correctly", originalData, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun kotlin_encrypt_decrypt_cyrillic_and_emoji() {
|
||||||
|
val originalData = "Привет мир! 🔐 Тест кириллицы и эмодзи 🚀"
|
||||||
|
val password = "пароль123"
|
||||||
|
|
||||||
|
val encrypted = CryptoManager.encryptWithPassword(originalData, password)
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
|
||||||
|
|
||||||
|
assertEquals("Cyrillic and emoji should roundtrip correctly", originalData, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun kotlin_encrypt_decrypt_json() {
|
||||||
|
val originalData = """{"key":"value","number":42,"nested":{"array":[1,2,3]}}"""
|
||||||
|
val password = "jsonPassword"
|
||||||
|
|
||||||
|
val encrypted = CryptoManager.encryptWithPassword(originalData, password)
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
|
||||||
|
|
||||||
|
assertEquals("JSON should roundtrip correctly", originalData, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun kotlin_wrong_password_returns_null() {
|
||||||
|
val originalData = "Secret message"
|
||||||
|
val correctPassword = "correctPassword"
|
||||||
|
val wrongPassword = "wrongPassword"
|
||||||
|
|
||||||
|
val encrypted = CryptoManager.encryptWithPassword(originalData, correctPassword)
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(encrypted, wrongPassword)
|
||||||
|
|
||||||
|
assertNull("Wrong password should return null", decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 🌐 Тесты JS → Kotlin (кросс-платформа)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun js_to_kotlin_decrypt_hello_world() {
|
||||||
|
if (JS_ENCRYPTED_HELLO_WORLD.isEmpty()) {
|
||||||
|
println("⚠️ SKIP: JS_ENCRYPTED_HELLO_WORLD not set. Generate from JS first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(JS_ENCRYPTED_HELLO_WORLD, "testPassword123")
|
||||||
|
|
||||||
|
assertEquals("Should decrypt JS data correctly", "Hello World", decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun js_to_kotlin_decrypt_json() {
|
||||||
|
if (JS_ENCRYPTED_JSON.isEmpty()) {
|
||||||
|
println("⚠️ SKIP: JS_ENCRYPTED_JSON not set. Generate from JS first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(JS_ENCRYPTED_JSON, "mySecretPass")
|
||||||
|
|
||||||
|
assertEquals("Should decrypt JSON from JS", """{"key":"value","number":42}""", decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun js_to_kotlin_decrypt_empty() {
|
||||||
|
if (JS_ENCRYPTED_EMPTY.isEmpty()) {
|
||||||
|
println("⚠️ SKIP: JS_ENCRYPTED_EMPTY not set. Generate from JS first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(JS_ENCRYPTED_EMPTY, "password")
|
||||||
|
|
||||||
|
assertEquals("Should decrypt empty string from JS", "", decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun js_to_kotlin_decrypt_cyrillic() {
|
||||||
|
if (JS_ENCRYPTED_CYRILLIC.isEmpty()) {
|
||||||
|
println("⚠️ SKIP: JS_ENCRYPTED_CYRILLIC not set. Generate from JS first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val decrypted = CryptoManager.decryptWithPassword(JS_ENCRYPTED_CYRILLIC, "пароль123")
|
||||||
|
|
||||||
|
assertEquals("Should decrypt Cyrillic from JS", "Привет мир! 🔐", decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 🔧 Тест формата данных
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encrypted_format_is_correct() {
|
||||||
|
val encrypted = CryptoManager.encryptWithPassword("test", "password")
|
||||||
|
|
||||||
|
val parts = encrypted.split(":")
|
||||||
|
assertEquals("Should have exactly 2 parts (iv:ct)", 2, parts.size)
|
||||||
|
|
||||||
|
// Проверяем что обе части - валидный Base64
|
||||||
|
try {
|
||||||
|
val iv = android.util.Base64.decode(parts[0], android.util.Base64.NO_WRAP)
|
||||||
|
val ct = android.util.Base64.decode(parts[1], android.util.Base64.NO_WRAP)
|
||||||
|
|
||||||
|
assertEquals("IV should be 16 bytes", 16, iv.size)
|
||||||
|
assertTrue("Ciphertext should not be empty", ct.isNotEmpty())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
fail("Both parts should be valid Base64: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 📊 Тест PBKDF2 ключа (для отладки)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pbkdf2_key_derivation_is_correct() {
|
||||||
|
// Известный тестовый вектор для PBKDF2-HMAC-SHA1
|
||||||
|
// password: "testPassword123", salt: "rosetta", iterations: 1000, keyLen: 32
|
||||||
|
|
||||||
|
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||||
|
val spec = javax.crypto.spec.PBEKeySpec(
|
||||||
|
"testPassword123".toCharArray(),
|
||||||
|
"rosetta".toByteArray(Charsets.UTF_8),
|
||||||
|
1000,
|
||||||
|
256
|
||||||
|
)
|
||||||
|
val key = factory.generateSecret(spec).encoded
|
||||||
|
|
||||||
|
// Выводим ключ для сравнения с JS
|
||||||
|
val keyHex = key.joinToString("") { "%02x".format(it) }
|
||||||
|
println("🔑 PBKDF2 Key (hex): $keyHex")
|
||||||
|
|
||||||
|
assertEquals("Key should be 32 bytes", 32, key.size)
|
||||||
|
|
||||||
|
// ⚠️ TODO: Сравните этот ключ с JS:
|
||||||
|
// const key = generatePBKDF2KeyJSI("testPassword123");
|
||||||
|
// console.log("Key (hex):", key.toString('hex'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,14 +139,24 @@ object CryptoManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt data with password using PBKDF2 + AES
|
* Encrypt data with password using PBKDF2 + AES
|
||||||
|
*
|
||||||
|
* ⚠️ ВАЖНО: Совместимость с JS (crypto-js) и React Native (cryptoJSI.ts):
|
||||||
|
* - PBKDF2WithHmacSHA1 (не SHA256!) - crypto-js использует SHA1 по умолчанию
|
||||||
|
* - Salt: "rosetta"
|
||||||
|
* - Iterations: 1000
|
||||||
|
* - Key size: 256 bit
|
||||||
|
* - AES-256-CBC с PKCS5/PKCS7 padding
|
||||||
|
* - Compression: zlib deflate (pako.deflate в JS)
|
||||||
|
* - Формат: base64(iv):base64(ciphertext)
|
||||||
*/
|
*/
|
||||||
fun encryptWithPassword(data: String, password: String): String {
|
fun encryptWithPassword(data: String, password: String): String {
|
||||||
// Compress data
|
// Compress data (zlib deflate - совместимо с pako.deflate в JS)
|
||||||
val compressed = compress(data.toByteArray())
|
val compressed = compress(data.toByteArray(Charsets.UTF_8))
|
||||||
|
|
||||||
// Derive key using PBKDF2
|
// Derive key using PBKDF2-HMAC-SHA1 (⚠️ SHA1, не SHA256!)
|
||||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
// crypto-js по умолчанию использует SHA1 для PBKDF2
|
||||||
val spec = PBEKeySpec(password.toCharArray(), SALT.toByteArray(), PBKDF2_ITERATIONS, KEY_SIZE)
|
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||||
|
val spec = PBEKeySpec(password.toCharArray(), SALT.toByteArray(Charsets.UTF_8), PBKDF2_ITERATIONS, KEY_SIZE)
|
||||||
val secretKey = factory.generateSecret(spec)
|
val secretKey = factory.generateSecret(spec)
|
||||||
val key = SecretKeySpec(secretKey.encoded, "AES")
|
val key = SecretKeySpec(secretKey.encoded, "AES")
|
||||||
|
|
||||||
@@ -169,6 +179,15 @@ object CryptoManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt data with password
|
* Decrypt data with password
|
||||||
|
*
|
||||||
|
* ⚠️ ВАЖНО: Совместимость с JS (crypto-js) и React Native (cryptoJSI.ts):
|
||||||
|
* - PBKDF2WithHmacSHA1 (не SHA256!) - crypto-js использует SHA1 по умолчанию
|
||||||
|
* - Salt: "rosetta"
|
||||||
|
* - Iterations: 1000
|
||||||
|
* - Key size: 256 bit
|
||||||
|
* - AES-256-CBC с PKCS5/PKCS7 padding
|
||||||
|
* - Decompression: zlib inflate (pako.inflate в JS)
|
||||||
|
* - Формат: base64(iv):base64(ciphertext)
|
||||||
*/
|
*/
|
||||||
fun decryptWithPassword(encryptedData: String, password: String): String? {
|
fun decryptWithPassword(encryptedData: String, password: String): String? {
|
||||||
return try {
|
return try {
|
||||||
@@ -178,20 +197,21 @@ object CryptoManager {
|
|||||||
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
||||||
val ciphertext = Base64.decode(parts[1], Base64.NO_WRAP)
|
val ciphertext = Base64.decode(parts[1], Base64.NO_WRAP)
|
||||||
|
|
||||||
// Derive key using PBKDF2
|
// Derive key using PBKDF2-HMAC-SHA1 (⚠️ SHA1, не SHA256!)
|
||||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||||
val spec = PBEKeySpec(password.toCharArray(), SALT.toByteArray(), PBKDF2_ITERATIONS, KEY_SIZE)
|
val spec = PBEKeySpec(password.toCharArray(), SALT.toByteArray(Charsets.UTF_8), PBKDF2_ITERATIONS, KEY_SIZE)
|
||||||
val secretKey = factory.generateSecret(spec)
|
val secretKey = factory.generateSecret(spec)
|
||||||
val key = SecretKeySpec(secretKey.encoded, "AES")
|
val key = SecretKeySpec(secretKey.encoded, "AES")
|
||||||
|
|
||||||
// Decrypt
|
// Decrypt with AES-256-CBC
|
||||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
|
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
|
||||||
val decrypted = cipher.doFinal(ciphertext)
|
val decrypted = cipher.doFinal(ciphertext)
|
||||||
|
|
||||||
// Decompress
|
// Decompress (zlib inflate - совместимо с pako.inflate в JS)
|
||||||
String(decompress(decrypted))
|
String(decompress(decrypted), Charsets.UTF_8)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("CryptoManager", "decryptWithPassword failed: ${e.message}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user