Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 15 additions & 18 deletions ExampleApp/ExampleApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ struct ContentView: View {
}

private var canCapture: Bool {
isSetup && hasActiveSession && !captureService.isCaptureInProgress
isSetup && hasActiveSession && isEnclaveReady && !captureService.isCaptureInProgress
}

// MARK: - Actions
Expand Down Expand Up @@ -475,21 +475,23 @@ struct ContentView: View {
return
}

guard isEnclaveReady else {
errorMessage = "Secure Enclave not available. Cannot capture without media integrity."
return
}

do {
// 1. Capture photo → JPEG bytes in memory
let photo = try await captureService.capturePhoto()
lastCapturedPhoto = photo
let capturedAt = photo.capturedAt

// 2. Generate media integrity (hash + sign) BEFORE saving to disk
var mediaIntegrity: MediaIntegrity?
if isEnclaveReady {
mediaIntegrity = try integrityService.generateIntegrity(
for: photo.jpegData,
captureId: session.captureId,
capturedAt: capturedAt
)
}
let mediaIntegrity = try integrityService.generateIntegrity(
for: photo.jpegData,
captureId: session.captureId,
capturedAt: capturedAt
)

// 3. Exchange nonce for trust token
isExchangingToken = true
Expand All @@ -498,15 +500,10 @@ struct ContentView: View {
isExchangingToken = false

// 4. Generate sidecar (JWT + media_integrity)
let sidecarData: Data
if let integrity = mediaIntegrity {
sidecarData = try sidecarGenerator.generate(
jwt: response.trustToken,
mediaIntegrity: integrity
)
} else {
sidecarData = try sidecarGenerator.generate(jwt: response.trustToken)
}
let sidecarData = try sidecarGenerator.generate(
jwt: response.trustToken,
mediaIntegrity: mediaIntegrity
)

// 5. Save photo + sidecar together
let url = try storage.save(photo)
Expand Down
34 changes: 4 additions & 30 deletions Sources/SignedShotSDK/Sidecar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,8 @@ public struct Sidecar: Codable, Sendable {
/// Capture trust information from the backend (ES256 signed JWT)
public let captureTrust: CaptureTrust

/// Media integrity proof from the device (optional, requires Secure Enclave)
public let mediaIntegrity: MediaIntegrity?

/// Initialize with JWT only (legacy, no media integrity)
public init(version: String = "1.0", jwt: String) {
self.version = version
self.captureTrust = CaptureTrust(jwt: jwt)
self.mediaIntegrity = nil
}
/// Media integrity proof from the device's Secure Enclave
public let mediaIntegrity: MediaIntegrity

/// Initialize with JWT and media integrity
public init(version: String = "1.0", jwt: String, mediaIntegrity: MediaIntegrity) {
Expand Down Expand Up @@ -51,15 +44,7 @@ public struct SidecarGenerator {
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
}

/// Generate sidecar JSON data (legacy, no media integrity)
/// - Parameter jwt: The trust token from the backend
/// - Returns: JSON data for the sidecar file
public func generate(jwt: String) throws -> Data {
let sidecar = Sidecar(jwt: jwt)
return try encoder.encode(sidecar)
}

/// Generate sidecar JSON data with media integrity
/// Generate sidecar JSON data
/// - Parameters:
/// - jwt: The trust token from the backend
/// - mediaIntegrity: The media integrity proof from Secure Enclave
Expand All @@ -69,18 +54,7 @@ public struct SidecarGenerator {
return try encoder.encode(sidecar)
}

/// Generate sidecar and return as string (legacy, no media integrity)
/// - Parameter jwt: The trust token from the backend
/// - Returns: JSON string for the sidecar file
public func generateString(jwt: String) throws -> String {
let data = try generate(jwt: jwt)
guard let string = String(data: data, encoding: .utf8) else {
throw SidecarError.encodingFailed
}
return string
}

/// Generate sidecar and return as string with media integrity
/// Generate sidecar and return as string
/// - Parameters:
/// - jwt: The trust token from the backend
/// - mediaIntegrity: The media integrity proof from Secure Enclave
Expand Down
31 changes: 29 additions & 2 deletions Tests/SignedShotSDKTests/APIModelsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,14 @@ final class APIModelsTests: XCTestCase {
// MARK: - Sidecar Tests

func testSidecarEncoding() throws {
let sidecar = Sidecar(jwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.test.sig")
let mediaIntegrity = MediaIntegrity(
contentHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
signature: "c2lnbmF0dXJl",
publicKey: "cHVibGljS2V5",
captureId: "550e8400-e29b-41d4-a716-446655440000",
capturedAt: "2026-01-26T15:30:00Z"
)
let sidecar = Sidecar(jwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.test.sig", mediaIntegrity: mediaIntegrity)

let encoder = JSONEncoder()
let data = try encoder.encode(sidecar)
Expand All @@ -150,6 +157,10 @@ final class APIModelsTests: XCTestCase {

let captureTrust = json?["capture_trust"] as? [String: Any]
XCTAssertEqual(captureTrust?["jwt"] as? String, "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.test.sig")

let integrity = json?["media_integrity"] as? [String: Any]
XCTAssertNotNil(integrity)
XCTAssertEqual(integrity?["capture_id"] as? String, "550e8400-e29b-41d4-a716-446655440000")
}

func testSidecarDecoding() throws {
Expand All @@ -158,6 +169,13 @@ final class APIModelsTests: XCTestCase {
"version": "1.0",
"capture_trust": {
"jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature"
},
"media_integrity": {
"content_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"signature": "c2lnbmF0dXJl",
"public_key": "cHVibGljS2V5",
"capture_id": "550e8400-e29b-41d4-a716-446655440000",
"captured_at": "2026-01-26T15:30:00Z"
}
}
"""
Expand All @@ -167,17 +185,26 @@ final class APIModelsTests: XCTestCase {

XCTAssertEqual(sidecar.version, "1.0")
XCTAssertEqual(sidecar.captureTrust.jwt, "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature")
XCTAssertEqual(sidecar.mediaIntegrity.captureId, "550e8400-e29b-41d4-a716-446655440000")
}

func testSidecarGenerator() throws {
let generator = SidecarGenerator()
let jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.test.signature"
let mediaIntegrity = MediaIntegrity(
contentHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
signature: "c2lnbmF0dXJl",
publicKey: "cHVibGljS2V5",
captureId: "550e8400-e29b-41d4-a716-446655440000",
capturedAt: "2026-01-26T15:30:00Z"
)

let jsonString = try generator.generateString(jwt: jwt)
let jsonString = try generator.generateString(jwt: jwt, mediaIntegrity: mediaIntegrity)

XCTAssertTrue(jsonString.contains("\"version\" : \"1.0\""))
XCTAssertTrue(jsonString.contains("\"capture_trust\""))
XCTAssertTrue(jsonString.contains(jwt))
XCTAssertTrue(jsonString.contains("\"media_integrity\""))
}

// MARK: - KeychainError Tests
Expand Down