mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-10 19:42:21 +00:00
152 lines
5.4 KiB
Swift
152 lines
5.4 KiB
Swift
import Foundation
|
|
import HapiClient
|
|
import Testing
|
|
|
|
/// The normative `hapi-push-v1` test vector (shared with the hub producer;
|
|
/// also published as `shared/fixtures/push/envelope-v1.json`). Hardcoded so
|
|
/// this suite stands alone: key = bytes 0x00…0x1f, nonce = bytes 0x00…0x0b.
|
|
enum PushVector {
|
|
static let key = Data((0..<32).map { UInt8($0) })
|
|
static let plaintext =
|
|
#"{"body":"Ready for input","contractVersion":"1","sessionId":"s1","title":"HAPI","type":"ready"}"#
|
|
static let envelopeBase64 =
|
|
"AAECAwQFBgcICQoLPCC0dKGc4CGvE/Lq1ZBYC+ykp12eCyoIGkvH5nIHdMBgc9qqyrNh8RvKXdeqtgoUzCoF/im/"
|
|
+ "zLR28wgjOpDEzNweshOnvUNDJnbiL7/GLMQa/rASE0FpSo+Y/6gB0sShxRvIOvgJGpKK+MjRlFlu"
|
|
|
|
/// The vector envelope with the byte at `index` XOR-flipped.
|
|
static func tampered(at index: Int) -> String {
|
|
var bytes = Data(base64Encoded: envelopeBase64)!
|
|
bytes[index] ^= 0x01
|
|
return bytes.base64EncodedString()
|
|
}
|
|
}
|
|
|
|
@Suite("Push envelope (hapi-push-v1)")
|
|
struct PushEnvelopeTests {
|
|
// MARK: - Structural validation (runs everywhere)
|
|
|
|
@Test func rejectsWrongKeyLength() {
|
|
#expect(throws: PushEnvelopeError.invalidKeyLength) {
|
|
try PushEnvelope.decrypt(
|
|
envelopeBase64: PushVector.envelopeBase64,
|
|
key: Data(count: 16)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test func rejectsNonBase64Envelope() {
|
|
#expect(throws: PushEnvelopeError.malformedEnvelope) {
|
|
try PushEnvelope.decrypt(envelopeBase64: "not-base64!!", key: PushVector.key)
|
|
}
|
|
}
|
|
|
|
@Test func rejectsTruncatedEnvelope() {
|
|
// 27 bytes < nonce (12) + tag (16): structurally impossible.
|
|
let short = Data(count: 27).base64EncodedString()
|
|
#expect(throws: PushEnvelopeError.malformedEnvelope) {
|
|
try PushEnvelope.decrypt(envelopeBase64: short, key: PushVector.key)
|
|
}
|
|
}
|
|
|
|
// MARK: - AES-GCM (CryptoKit; Darwin-only, skipped in the Linux container)
|
|
|
|
#if canImport(CryptoKit)
|
|
@Test func decryptsTheNormativeVector() throws {
|
|
let plaintext = try PushEnvelope.decrypt(
|
|
envelopeBase64: PushVector.envelopeBase64,
|
|
key: PushVector.key
|
|
)
|
|
#expect(String(data: plaintext, encoding: .utf8) == PushVector.plaintext)
|
|
}
|
|
|
|
@Test func vectorPlaintextParsesToPayload() throws {
|
|
let plaintext = try PushEnvelope.decrypt(
|
|
envelopeBase64: PushVector.envelopeBase64,
|
|
key: PushVector.key
|
|
)
|
|
let payload = try #require(PushPayload.parse(plaintext: plaintext))
|
|
#expect(payload.type == .ready)
|
|
#expect(payload.sessionId == "s1")
|
|
#expect(payload.title == "HAPI")
|
|
#expect(payload.body == "Ready for input")
|
|
#expect(payload.contractVersion == "1")
|
|
#expect(payload.isKnownContractVersion)
|
|
#expect(payload.supportsActions)
|
|
#expect(payload.categoryIdentifier == "ready")
|
|
}
|
|
|
|
@Test func rejectsTamperedCiphertext() {
|
|
// Byte 20 sits inside the ciphertext region (after the 12-byte nonce).
|
|
#expect(throws: PushEnvelopeError.authenticationFailed) {
|
|
try PushEnvelope.decrypt(
|
|
envelopeBase64: PushVector.tampered(at: 20),
|
|
key: PushVector.key
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test func rejectsTamperedTag() {
|
|
let last = Data(base64Encoded: PushVector.envelopeBase64)!.count - 1
|
|
#expect(throws: PushEnvelopeError.authenticationFailed) {
|
|
try PushEnvelope.decrypt(
|
|
envelopeBase64: PushVector.tampered(at: last),
|
|
key: PushVector.key
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test func rejectsTamperedNonce() {
|
|
#expect(throws: PushEnvelopeError.authenticationFailed) {
|
|
try PushEnvelope.decrypt(
|
|
envelopeBase64: PushVector.tampered(at: 0),
|
|
key: PushVector.key
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test func rejectsWrongKey() {
|
|
var wrong = PushVector.key
|
|
wrong[0] ^= 0xFF
|
|
#expect(throws: PushEnvelopeError.authenticationFailed) {
|
|
try PushEnvelope.decrypt(envelopeBase64: PushVector.envelopeBase64, key: wrong)
|
|
}
|
|
}
|
|
#else
|
|
@Test func decryptReportsCryptoUnavailable() {
|
|
// Linux container: structure is validated, the AES-GCM open is not
|
|
// implemented (no CryptoKit, and no new dependencies allowed).
|
|
#expect(throws: PushEnvelopeError.cryptoUnavailable) {
|
|
try PushEnvelope.decrypt(
|
|
envelopeBase64: PushVector.envelopeBase64,
|
|
key: PushVector.key
|
|
)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
@Suite("Push device key")
|
|
struct PushDeviceKeyTests {
|
|
@Test func generatesDistinct32ByteKeys() {
|
|
let first = PushDeviceKey.generate()
|
|
let second = PushDeviceKey.generate()
|
|
#expect(first.data.count == 32)
|
|
#expect(second.data.count == 32)
|
|
#expect(first != second)
|
|
}
|
|
|
|
@Test func base64RoundTrips() throws {
|
|
let key = PushDeviceKey.generate()
|
|
let restored = try #require(PushDeviceKey(base64: key.base64))
|
|
#expect(restored == key)
|
|
#expect(restored.base64 == key.base64)
|
|
}
|
|
|
|
@Test func rejectsWrongLengthOrGarbage() {
|
|
#expect(PushDeviceKey(data: Data(count: 31)) == nil)
|
|
#expect(PushDeviceKey(data: Data(count: 33)) == nil)
|
|
#expect(PushDeviceKey(base64: Data(count: 16).base64EncodedString()) == nil)
|
|
#expect(PushDeviceKey(base64: "***") == nil)
|
|
}
|
|
}
|