Closer/iphone/CloserTests/CryptoTests/CoupleEncryptionManagerTest...

87 lines
4.1 KiB
Swift

import XCTest
import CryptoKit
@testable import Closer
final class CoupleEncryptionManagerTests: XCTestCase {
func testWrapUnwrapRoundTrip() throws {
let key = try CoupleEncryptionManager.generateCoupleKey()
let phrase = try RecoveryKeyManager.generatePhrase()
let wrapped = try CoupleEncryptionManager.wrap(key, with: phrase)
XCTAssertEqual(wrapped.kdfParams, CoupleEncryptionManager.kdfParamsTag)
XCTAssertEqual(wrapped.kdfSalt.count, CoupleEncryptionManager.saltBytes)
// Ciphertext includes nonce (12) + at least 1 byte plaintext + tag (16).
XCTAssertGreaterThanOrEqual(wrapped.ciphertext.count, 29)
let unwrapped = try CoupleEncryptionManager.unwrap(wrapped, with: phrase)
XCTAssertEqual(unwrapped.rawKey.bytes, key.rawKey.bytes)
}
func testBadPhraseRejects() throws {
let key = try CoupleEncryptionManager.generateCoupleKey()
let phrase = try RecoveryKeyManager.generatePhrase()
let wrapped = try CoupleEncryptionManager.wrap(key, with: phrase)
let wrongPhrase = RecoveryKeyManager.normalize(phrase) + " wrong"
XCTAssertThrowsError(try CoupleEncryptionManager.unwrap(wrapped, with: wrongPhrase))
}
func testInvitePhraseEncryptionRoundTrip() throws {
let phrase = try RecoveryKeyManager.generatePhrase()
let inviteCode = "ABC123"
let blob = try CoupleEncryptionManager.encryptRecoveryPhrase(phrase, with: inviteCode)
let recovered = try CoupleEncryptionManager.decryptRecoveryPhrase(blob, with: inviteCode)
XCTAssertEqual(recovered, phrase)
}
func testInvitePhraseBadCodeRejects() throws {
let phrase = try RecoveryKeyManager.generatePhrase()
let blob = try CoupleEncryptionManager.encryptRecoveryPhrase(phrase, with: "ABC123")
XCTAssertThrowsError(try CoupleEncryptionManager.decryptRecoveryPhrase(blob, with: "ABC124"))
}
/// Known-vector test (iOS self-consistency).
///
/// Uses the first 10 words of the bundled wordlist and a deterministic salt.
/// The expected SHA-256 of the unwrapped key is a placeholder until the test
/// can be executed on macOS/CI where libsodium Argon2id is available.
///
/// TODO(Batch 3 follow-up): replace `expectedHash` with the real output from
/// a Mac/CI run, then add a matching BouncyCastle vector from Android.
func testKnownVectorUnwrapPlaceholder() throws {
let words = try Wordlist.load()
let phrase = words.prefix(10).joined(separator: " ")
let salt = Data((0x00...0x0F).map { $0 })
let key = CoupleKeyMaterial(rawBytes: Data(repeating: 0xCD, count: 32))
let kek = try CoupleEncryptionManager.unwrapKEK(phrase: phrase, salt: salt)
let wrappedCiphertext = try FieldEncryptor.encrypt(
key.rawKey.bytes,
key: kek,
aad: CoupleEncryptionManager.coupleKeyAAD.data(using: .utf8)
)
let wrapped = WrappedCoupleKey(
ciphertext: wrappedCiphertext,
kdfSalt: salt,
kdfParams: CoupleEncryptionManager.kdfParamsTag
)
let unwrapped = try CoupleEncryptionManager.unwrap(wrapped, with: phrase)
let hash = SHA256.hash(data: unwrapped.rawKey.bytes)
let hashHex = hash.compactMap { String(format: "%02x", $0) }.joined()
// Placeholder: libsodium Argon2id cannot run in this Linux environment.
// Replace with the real hash after a Mac/CI run.
let placeholderHash = "0000000000000000000000000000000000000000000000000000000000000000"
XCTAssertNotEqual(hashHex, placeholderHash, "Expected hash placeholder must be updated on macOS/CI")
// The real assertion (commented out until the Mac/CI run provides the value):
// let expectedHash = "REPLACE_WITH_MAC_CI_HASH"
// XCTAssertEqual(hashHex, expectedHash)
// Document cross-platform gap in the test output.
XCTAssertTrue(
hashHex.count == 64,
"Hash must be 64 hex chars. Cross-platform BouncyCastle↔libsodium verification requires a paired CI run (Android emulator + iOS simulator + shared fixture)."
)
}
}