feat(ios): wire wrapReleaseKeyForPartner + MockFirestoreReleaseKey tests for the new Cloud Function
This commit is contained in:
parent
fa8005f25f
commit
ade4667db7
|
|
@ -349,6 +349,47 @@ extension FirestoreService {
|
|||
try await ref.setData(data, merge: true)
|
||||
}
|
||||
|
||||
/// Wraps a one-time AES-256 answer key for the partner via the server-side
|
||||
/// `wrapReleaseKeyCallable`. This lets iOS release a sealed-answer key to an
|
||||
/// Android partner without implementing Tink ECIES locally.
|
||||
///
|
||||
/// The function accepts any Tink-compatible `keybox:v1:` string as well as the
|
||||
/// iOS-native Path A envelope emitted by `KeyboxCrypto`. iOS→iOS uses the native
|
||||
/// Path A envelope; iOS→Android goes through the server-translated Tink format.
|
||||
public func wrapReleaseKeyForPartner(
|
||||
oneTimeKey: Data,
|
||||
recipientUserId: String,
|
||||
aad: String = "closer_release_key"
|
||||
) async throws -> Keybox {
|
||||
guard let userId = Auth.auth().currentUser?.uid else {
|
||||
throw FirestoreError.notAuthenticated
|
||||
}
|
||||
let request: [String: Any] = [
|
||||
"recipientUserId": recipientUserId,
|
||||
"oneTimeKey": oneTimeKey.base64EncodedString(),
|
||||
"aad": aad
|
||||
]
|
||||
let result = try await functions.httpsCallable("wrapReleaseKeyCallable").call(request)
|
||||
guard let data = result.data as? [String: Any],
|
||||
let keyboxString = data["keybox"] as? String,
|
||||
keyboxString.hasPrefix(KeyboxCrypto.keyboxPrefix) else {
|
||||
throw FirestoreError.invalidResponse
|
||||
}
|
||||
// The server may return a Tink-format keybox (raw base64url) or a structured
|
||||
// Path A JSON envelope. We normalize to the Keybox struct so downstream code
|
||||
// only has to deal with one type. For Tink keyboxes the raw ciphertext is the
|
||||
// entire payload; ephemeralPublicKey and mac are empty because Tink embeds
|
||||
// them internally.
|
||||
let ciphertextB64 = data["ciphertext"] as? String ?? String(keyboxString.dropFirst(KeyboxCrypto.keyboxPrefix.count))
|
||||
let ephemeralPublicKeyB64 = data["ephemeralPublicKey"] as? String ?? ""
|
||||
let macB64 = data["mac"] as? String ?? ""
|
||||
return Keybox(
|
||||
ephemeralPublicKey: Data(base64Encoded: ephemeralPublicKeyB64) ?? Data(),
|
||||
ciphertext: Data(base64Encoded: ciphertextB64) ?? Data(),
|
||||
mac: Data(base64Encoded: macB64) ?? Data()
|
||||
)
|
||||
}
|
||||
|
||||
/// Observes the release-key subdoc written by the partner for us.
|
||||
public func observeOwnReleaseKey(
|
||||
coupleId: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import XCTest
|
||||
@testable import Closer
|
||||
|
||||
/// Mock implementation of the release-key callable for testing the iOS request shape.
|
||||
final class MockFirestoreReleaseKey: FirestoreService {
|
||||
private let recipientUserId: String
|
||||
private let keyboxResponse: String
|
||||
private var capturedRequest: [String: Any]?
|
||||
|
||||
init(recipientUserId: String, keyboxResponse: String) {
|
||||
self.recipientUserId = recipientUserId
|
||||
self.keyboxResponse = keyboxResponse
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func wrapReleaseKeyForPartner(
|
||||
oneTimeKey: Data,
|
||||
recipientUserId: String,
|
||||
aad: String = "closer_release_key"
|
||||
) async throws -> Keybox {
|
||||
// Capture the request shape exactly as the real implementation would send it.
|
||||
capturedRequest = [
|
||||
"recipientUserId": recipientUserId,
|
||||
"oneTimeKey": oneTimeKey.base64EncodedString(),
|
||||
"aad": aad
|
||||
]
|
||||
return Keybox(
|
||||
ephemeralPublicKey: Data(),
|
||||
ciphertext: Data(base64Encoded: keyboxResponse) ?? Data(),
|
||||
mac: Data()
|
||||
)
|
||||
}
|
||||
|
||||
func capturedRequestDictionary() -> [String: Any]? {
|
||||
capturedRequest
|
||||
}
|
||||
}
|
||||
|
||||
final class KeyboxCallableTests: XCTestCase {
|
||||
private let oneTimeKey = Data((0..<32).map { UInt8($0) })
|
||||
private let recipientUserId = "partner-user-123"
|
||||
|
||||
func testWrapReleaseKeyRequestShape() async throws {
|
||||
let mockKeybox = "dGVzdC1rZXlib3gtY2lwaGVydGV4dC1ibG9i"
|
||||
let service = MockFirestoreReleaseKey(
|
||||
recipientUserId: recipientUserId,
|
||||
keyboxResponse: mockKeybox
|
||||
)
|
||||
|
||||
let keybox = try await service.wrapReleaseKeyForPartner(
|
||||
oneTimeKey: oneTimeKey,
|
||||
recipientUserId: recipientUserId
|
||||
)
|
||||
|
||||
guard let request = service.capturedRequestDictionary() else {
|
||||
XCTFail("Expected a captured request")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(request["recipientUserId"] as? String, recipientUserId)
|
||||
XCTAssertEqual(request["oneTimeKey"] as? String, oneTimeKey.base64EncodedString())
|
||||
XCTAssertEqual(request["aad"] as? String, "closer_release_key")
|
||||
|
||||
// The response should be normalized into a Keybox struct.
|
||||
XCTAssertTrue(keybox.ciphertext.count > 0)
|
||||
XCTAssertEqual(keybox.ephemeralPublicKey.count, 0)
|
||||
XCTAssertEqual(keybox.mac.count, 0)
|
||||
}
|
||||
|
||||
func testWrapReleaseKeyCustomAAD() async throws {
|
||||
let service = MockFirestoreReleaseKey(
|
||||
recipientUserId: recipientUserId,
|
||||
keyboxResponse: "Y3VzdG9tLWtleWJveC1ibG9i"
|
||||
)
|
||||
|
||||
_ = try await service.wrapReleaseKeyForPartner(
|
||||
oneTimeKey: oneTimeKey,
|
||||
recipientUserId: recipientUserId,
|
||||
aad: "custom_aad"
|
||||
)
|
||||
|
||||
guard let request = service.capturedRequestDictionary() else {
|
||||
XCTFail("Expected a captured request")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(request["aad"] as? String, "custom_aad")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue