Чат: вертикальное центрирование bubble вложений, tap-to-download аватар и мгновенный показ call-attachment

This commit is contained in:
2026-03-29 15:29:13 +05:00
parent 6e927f8871
commit 3b26176875
218 changed files with 14952 additions and 237 deletions

View File

@@ -0,0 +1,229 @@
import XCTest
@testable import Rosetta
/// Tests that iOS crypto primitives produce byte-identical output
/// to Android/Desktop for call E2EE.
final class CallCryptoParityTests: XCTestCase {
// MARK: - HSalsa20 (nacl.box.before parity)
func testHSalsa20ProducesConsistentOutput() throws {
// HSalsa20 with zero nonce should produce deterministic output from any key
let key = Data(repeating: 0x42, count: 32)
let result1 = NativeCryptoBridge.naclSharedKey(fromRawDH: key)
let result2 = NativeCryptoBridge.naclSharedKey(fromRawDH: key)
XCTAssertNotNil(result1)
XCTAssertNotNil(result2)
XCTAssertEqual(result1!.count, 32)
XCTAssertEqual(result1, result2, "HSalsa20 must be deterministic")
}
func testHSalsa20OutputDiffersForDifferentKeys() throws {
let key1 = Data(repeating: 0x01, count: 32)
let key2 = Data(repeating: 0x02, count: 32)
let result1 = NativeCryptoBridge.naclSharedKey(fromRawDH: key1)
let result2 = NativeCryptoBridge.naclSharedKey(fromRawDH: key2)
XCTAssertNotNil(result1)
XCTAssertNotNil(result2)
XCTAssertNotEqual(result1, result2, "Different keys must produce different shared secrets")
}
func testHSalsa20RejectsWrongKeyLength() {
let shortKey = Data(repeating: 0x00, count: 16)
let result = NativeCryptoBridge.naclSharedKey(fromRawDH: shortKey)
XCTAssertNil(result, "HSalsa20 must reject keys != 32 bytes")
}
// MARK: - XChaCha20 stream cipher
func testXChaCha20EncryptDecryptRoundTrip() throws {
let key = Data(repeating: 0xAB, count: 32)
let nonce = Data(repeating: 0xCD, count: 24)
let plaintext = Data("Hello Rosetta E2EE call frame!".utf8)
let encrypted = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce)
XCTAssertNotNil(encrypted)
XCTAssertEqual(encrypted!.count, plaintext.count, "Encrypted size must match plaintext size")
XCTAssertNotEqual(encrypted, plaintext, "Encrypted data must differ from plaintext")
// XOR cipher: encrypt again = decrypt
let decrypted = NativeCryptoBridge.xChaCha20Xor(encrypted!, key: key, nonce: nonce)
XCTAssertNotNil(decrypted)
XCTAssertEqual(decrypted, plaintext, "XChaCha20 XOR decrypt must recover original plaintext")
}
func testXChaCha20DifferentNonceProducesDifferentCiphertext() throws {
let key = Data(repeating: 0x11, count: 32)
let nonce1 = Data(repeating: 0x01, count: 24)
let nonce2 = Data(repeating: 0x02, count: 24)
let plaintext = Data(repeating: 0xFF, count: 64)
let enc1 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce1)
let enc2 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce2)
XCTAssertNotNil(enc1)
XCTAssertNotNil(enc2)
XCTAssertNotEqual(enc1, enc2, "Different nonces must produce different ciphertext")
}
func testXChaCha20DifferentKeyProducesDifferentCiphertext() throws {
let key1 = Data(repeating: 0x11, count: 32)
let key2 = Data(repeating: 0x22, count: 32)
let nonce = Data(repeating: 0x00, count: 24)
let plaintext = Data(repeating: 0xFF, count: 64)
let enc1 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key1, nonce: nonce)
let enc2 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key2, nonce: nonce)
XCTAssertNotNil(enc1)
XCTAssertNotNil(enc2)
XCTAssertNotEqual(enc1, enc2, "Different keys must produce different ciphertext")
}
func testXChaCha20OutputSizeMatchesInput() throws {
let key = Data(repeating: 0x33, count: 32)
let nonce = Data(repeating: 0x44, count: 24)
for size in [0, 1, 16, 63, 64, 65, 128, 960, 1024] {
let plaintext = Data(repeating: 0xAA, count: size)
let encrypted = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce)
XCTAssertNotNil(encrypted, "XChaCha20 must handle size \(size)")
XCTAssertEqual(encrypted!.count, size, "Output size must equal input size for \(size) bytes")
}
}
func testXChaCha20RejectsWrongKeyOrNonceLength() {
let plaintext = Data(repeating: 0x00, count: 16)
let badKey = NativeCryptoBridge.xChaCha20Xor(plaintext, key: Data(repeating: 0, count: 16), nonce: Data(repeating: 0, count: 24))
XCTAssertNil(badKey, "Must reject key != 32 bytes")
let badNonce = NativeCryptoBridge.xChaCha20Xor(plaintext, key: Data(repeating: 0, count: 32), nonce: Data(repeating: 0, count: 12))
XCTAssertNil(badNonce, "Must reject nonce != 24 bytes")
}
// MARK: - Nonce construction parity (Desktop BigInt encoding)
/// Verifies that a nonce built from an 8-byte big-endian timestamp
/// matches the Desktop audioE2EE.ts fillNonceFromTimestamp output.
func testNonceFromTimestampMatchesDesktopFormat() throws {
let key = Data(repeating: 0x55, count: 32)
let plaintext = Data(repeating: 0xEE, count: 48)
// Desktop encodes timestamp 12345 as big-endian 8 bytes:
// [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x39]
// Then nonce[8:24] = zeros
let timestamp: UInt64 = 12345
var nonce = Data(repeating: 0, count: 24)
nonce[0] = UInt8((timestamp >> 56) & 0xFF)
nonce[1] = UInt8((timestamp >> 48) & 0xFF)
nonce[2] = UInt8((timestamp >> 40) & 0xFF)
nonce[3] = UInt8((timestamp >> 32) & 0xFF)
nonce[4] = UInt8((timestamp >> 24) & 0xFF)
nonce[5] = UInt8((timestamp >> 16) & 0xFF)
nonce[6] = UInt8((timestamp >> 8) & 0xFF)
nonce[7] = UInt8(timestamp & 0xFF)
// Encrypt with manually constructed nonce
let enc1 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce)
XCTAssertNotNil(enc1)
// Same nonce built differently (raw bytes = big-endian 12345)
var nonce2 = Data(repeating: 0, count: 24)
var tsBE = timestamp.bigEndian
nonce2.replaceSubrange(0..<8, with: Data(bytes: &tsBE, count: 8))
let enc2 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce2)
XCTAssertNotNil(enc2)
XCTAssertEqual(enc1, enc2, "Both nonce construction methods must produce identical ciphertext")
}
/// Verifies that 32-bit RTP timestamp at nonce[4:8] matches
/// the 64-bit encoding when value fits in 32 bits.
func testRtpTimestamp32MatchesDesktop64ForSmallValues() throws {
let key = Data(repeating: 0x66, count: 32)
let plaintext = Data(repeating: 0xDD, count: 32)
let rtpTs: UInt32 = 48000 // 1 second at 48kHz
// Desktop-style: 64-bit big-endian in nonce[0:8]
var nonceDesktop = Data(repeating: 0, count: 24)
let ts64 = UInt64(rtpTs)
nonceDesktop[0] = UInt8((ts64 >> 56) & 0xFF) // 0
nonceDesktop[1] = UInt8((ts64 >> 48) & 0xFF) // 0
nonceDesktop[2] = UInt8((ts64 >> 40) & 0xFF) // 0
nonceDesktop[3] = UInt8((ts64 >> 32) & 0xFF) // 0
nonceDesktop[4] = UInt8((ts64 >> 24) & 0xFF) // 0
nonceDesktop[5] = UInt8((ts64 >> 16) & 0xFF) // 0
nonceDesktop[6] = UInt8((ts64 >> 8) & 0xFF) // 0xBB
nonceDesktop[7] = UInt8(ts64 & 0xFF) // 0x80
// RTP-style: 32-bit big-endian at nonce[4:8]
var nonceRtp = Data(repeating: 0, count: 24)
nonceRtp[4] = UInt8((rtpTs >> 24) & 0xFF) // 0
nonceRtp[5] = UInt8((rtpTs >> 16) & 0xFF) // 0
nonceRtp[6] = UInt8((rtpTs >> 8) & 0xFF) // 0xBB
nonceRtp[7] = UInt8(rtpTs & 0xFF) // 0x80
let encDesktop = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonceDesktop)
let encRtp = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonceRtp)
XCTAssertNotNil(encDesktop)
XCTAssertNotNil(encRtp)
XCTAssertEqual(encDesktop, encRtp,
"32-bit RTP timestamp at nonce[4:8] must match 64-bit encoding for values < 2^32")
}
// MARK: - Full E2EE round-trip (simulate iOS encrypt Desktop decrypt)
func testFullE2EERoundTripWithDesktopNonce() throws {
// Simulate: iOS encrypts a frame, Desktop decrypts with same key and nonce
let sharedKey = Data(repeating: 0x77, count: 32)
// Simulated Opus frame (TOC byte 0xFC = CELT, 20ms, 1 frame)
var opusFrame = Data([0xFC]) // TOC: config=31, code=0
opusFrame.append(Data(repeating: 0x42, count: 80)) // payload
// Desktop-style timestamp nonce
let timestamp: UInt64 = 96000 // 2 seconds at 48kHz
var nonce = Data(repeating: 0, count: 24)
for i in 0..<8 {
nonce[i] = UInt8((timestamp >> UInt64((7 - i) * 8)) & 0xFF)
}
// Encrypt (iOS side)
let encrypted = NativeCryptoBridge.xChaCha20Xor(opusFrame, key: sharedKey, nonce: nonce)
XCTAssertNotNil(encrypted)
XCTAssertEqual(encrypted!.count, opusFrame.count)
XCTAssertNotEqual(encrypted, opusFrame)
// Decrypt (simulating Desktop side with same key+nonce)
let decrypted = NativeCryptoBridge.xChaCha20Xor(encrypted!, key: sharedKey, nonce: nonce)
XCTAssertNotNil(decrypted)
XCTAssertEqual(decrypted, opusFrame, "Desktop must recover exact Opus frame")
}
// MARK: - HSalsa20 + XChaCha20 full chain
func testFullKeyDerivationAndEncryptionChain() throws {
// Simulate two peers with fixed "DH results"
let rawDH = Data((0..<32).map { UInt8($0) })
let sharedKey = NativeCryptoBridge.naclSharedKey(fromRawDH: rawDH)
XCTAssertNotNil(sharedKey)
XCTAssertEqual(sharedKey!.count, 32)
let plaintext = Data(repeating: 0xAA, count: 160) // typical Opus frame
let nonce = Data(repeating: 0, count: 24)
let encrypted = NativeCryptoBridge.xChaCha20Xor(plaintext, key: sharedKey!, nonce: nonce)
XCTAssertNotNil(encrypted)
let decrypted = NativeCryptoBridge.xChaCha20Xor(encrypted!, key: sharedKey!, nonce: nonce)
XCTAssertEqual(decrypted, plaintext, "Full chain: HSalsa20 key derivation + XChaCha20 must round-trip")
}
}