114 lines
4.0 KiB
Swift
114 lines
4.0 KiB
Swift
import XCTest
|
|
import CryptoKit
|
|
@testable import Closer
|
|
|
|
final class AnswerCryptoTests: XCTestCase {
|
|
private let coupleId = "couple-test-456"
|
|
private let userId = "user-test-789"
|
|
private let questionId = "question-test-abc"
|
|
private var key: CoupleKeyMaterial {
|
|
CoupleKeyMaterial(rawBytes: Data(repeating: 0xAB, count: 32))
|
|
}
|
|
|
|
func testEncryptDecryptRoundTrip() throws {
|
|
let plaintext = "This is my private answer."
|
|
let payload = try AnswerCrypto.encrypt(
|
|
answerPlaintext: plaintext,
|
|
userId: userId,
|
|
questionId: questionId,
|
|
coupleId: coupleId,
|
|
key: key
|
|
)
|
|
XCTAssertEqual(payload.schemaVersion, AnswerCrypto.schemaVersion)
|
|
XCTAssertTrue(payload.ciphertext.hasPrefix(FieldEncryptor.prefix))
|
|
|
|
let recovered = try AnswerCrypto.decrypt(payload, key: key)
|
|
XCTAssertEqual(recovered, plaintext)
|
|
}
|
|
|
|
func testEncodeDecodeRoundTrip() throws {
|
|
let plaintext = "Round-trip via outer wrapper"
|
|
let payload = try AnswerCrypto.encrypt(
|
|
answerPlaintext: plaintext,
|
|
userId: userId,
|
|
questionId: questionId,
|
|
coupleId: coupleId,
|
|
key: key
|
|
)
|
|
let encoded = try AnswerCrypto.encode(payload)
|
|
XCTAssertTrue(encoded.hasPrefix(FieldEncryptor.prefix))
|
|
|
|
let decoded = try AnswerCrypto.decode(encoded)
|
|
XCTAssertEqual(decoded.userId, userId)
|
|
XCTAssertEqual(decoded.questionId, questionId)
|
|
XCTAssertEqual(decoded.coupleId, coupleId)
|
|
XCTAssertEqual(decoded.schemaVersion, AnswerCrypto.schemaVersion)
|
|
|
|
let recovered = try AnswerCrypto.decrypt(decoded, key: key)
|
|
XCTAssertEqual(recovered, plaintext)
|
|
}
|
|
|
|
func testAADMismatchRejects() throws {
|
|
let plaintext = "AAD-bound answer"
|
|
let payload = try AnswerCrypto.encrypt(
|
|
answerPlaintext: plaintext,
|
|
userId: userId,
|
|
questionId: questionId,
|
|
coupleId: coupleId,
|
|
key: key
|
|
)
|
|
// Tamper the questionId in the payload so the AAD would differ.
|
|
var tampered = payload
|
|
tampered = SecureAnswerPayload(
|
|
schemaVersion: payload.schemaVersion,
|
|
coupleId: payload.coupleId,
|
|
userId: payload.userId,
|
|
questionId: payload.questionId + "x",
|
|
ciphertext: payload.ciphertext,
|
|
createdAt: payload.createdAt
|
|
)
|
|
XCTAssertThrowsError(try AnswerCrypto.decrypt(tampered, key: key))
|
|
}
|
|
|
|
func testTamperedCiphertextRejects() throws {
|
|
let plaintext = "Tamper me"
|
|
let payload = try AnswerCrypto.encrypt(
|
|
answerPlaintext: plaintext,
|
|
userId: userId,
|
|
questionId: questionId,
|
|
coupleId: coupleId,
|
|
key: key
|
|
)
|
|
var chars = Array(payload.ciphertext)
|
|
let prefixEnd = FieldEncryptor.prefix.count
|
|
chars[prefixEnd + 5] ^= 0x01
|
|
let tamperedCiphertext = String(chars)
|
|
let tamperedPayload = SecureAnswerPayload(
|
|
schemaVersion: payload.schemaVersion,
|
|
coupleId: payload.coupleId,
|
|
userId: payload.userId,
|
|
questionId: payload.questionId,
|
|
ciphertext: tamperedCiphertext,
|
|
createdAt: payload.createdAt
|
|
)
|
|
XCTAssertThrowsError(try AnswerCrypto.decrypt(tamperedPayload, key: key))
|
|
}
|
|
|
|
func testOuterWrapperTamperRejects() throws {
|
|
let plaintext = "Outer tamper"
|
|
let payload = try AnswerCrypto.encrypt(
|
|
answerPlaintext: plaintext,
|
|
userId: userId,
|
|
questionId: questionId,
|
|
coupleId: coupleId,
|
|
key: key
|
|
)
|
|
let encoded = try AnswerCrypto.encode(payload)
|
|
var chars = Array(encoded)
|
|
let prefixEnd = FieldEncryptor.prefix.count
|
|
chars[prefixEnd + 3] ^= 0x01
|
|
let tampered = String(chars)
|
|
XCTAssertThrowsError(try AnswerCrypto.decode(tampered))
|
|
}
|
|
}
|