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
8 changes: 7 additions & 1 deletion OpenTDFKit/CryptoHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,13 @@ public actor CryptoHelper {
/// - Throws: `CryptoKitError` if the AES-GCM seal operation fails.
func createGMACBinding(policyBody: Data, symmetricKey: SymmetricKey) throws -> Data {
// Seal empty data, authenticating the policyBody. The tag is the GMAC binding.
let sealedBox = try CryptoKit.AES.GCM.seal(Data(), using: symmetricKey, authenticating: policyBody)
// Use a deterministic zero nonce so the binding can be recomputed and verified later.
// Security note: this is safe only because every NanoTDF derives a unique symmetric key
// from a fresh ephemeral ECDH key. Reusing a symmetric key with a fixed zero nonce while
// also encrypting payloads under the same key would create a catastrophic AES-GCM nonce-reuse
// condition. Do not cache or reuse symmetric keys across NanoTDFs.
let nonce = try CryptoKit.AES.GCM.Nonce(data: Data(count: CryptoConstants.aesGcmNonceSize))
let sealedBox = try CryptoKit.AES.GCM.seal(Data(), using: symmetricKey, nonce: nonce, authenticating: policyBody)
// Truncate to 64 bits (8 bytes) per spec section 3.3.1.3
return Data(sealedBox.tag.prefix(8))
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Expand Down
6 changes: 4 additions & 2 deletions OpenTDFKit/KASService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,10 @@ public actor KASService {
policyData: Data,
symmetricKey: SymmetricKey,
) async throws -> Bool {
// For GMAC binding verification, create a tag with empty ciphertext and the policy data as authenticated data
let fullTag = try AES.GCM.seal(Data(), using: symmetricKey, authenticating: policyData).tag
// For GMAC binding verification, create a tag with empty ciphertext and the policy data as authenticated data.
// Use the same deterministic zero nonce that createGMACBinding uses so the tag can be reproduced.
let nonce = try AES.GCM.Nonce(data: Data(count: 12))
let fullTag = try AES.GCM.seal(Data(), using: symmetricKey, nonce: nonce, authenticating: policyData).tag
guard policyBinding.count == 8 || policyBinding.count == fullTag.count else {
return false
}
Expand Down
91 changes: 91 additions & 0 deletions OpenTDFKitTests/BinaryParserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Foundation
@testable import OpenTDFKit
import XCTest

final class BinaryParserTests: XCTestCase {
func testInvalidMagicNumberThrows() {
let data = Data([0x00, 0x00, 0x4C])
let parser = BinaryParser(data: data)

XCTAssertThrowsError(try parser.parseHeader()) { error in
guard let parsingError = error as? ParsingError,
case .invalidMagicNumber = parsingError
else {
XCTFail("Expected ParsingError.invalidMagicNumber, got \(error)")
return
}
}
}

func testInvalidVersionThrows() {
// Valid magic number ("L1") followed by unsupported v13 (0x4D)
let data = Data([0x4C, 0x31, 0x4D])
let parser = BinaryParser(data: data)

XCTAssertThrowsError(try parser.parseHeader()) { error in
guard let parsingError = error as? ParsingError,
case .invalidVersion = parsingError
else {
XCTFail("Expected ParsingError.invalidVersion, got \(error)")
return
}
}
}

func testTruncatedHeaderThrowsInvalidFormat() {
// Magic + version only; missing KAS locator, ECC mode, payload config, policy, and ephemeral key
let data = Data([0x4C, 0x31, 0x4C])
let parser = BinaryParser(data: data)

XCTAssertThrowsError(try parser.parseHeader()) { error in
guard let parsingError = error as? ParsingError,
case .invalidFormat = parsingError
else {
XCTFail("Expected ParsingError.invalidFormat, got \(error)")
return
}
}
}

func testMissingEphemeralKeyThrowsInvalidFormat() {
// Valid magic + version + valid KAS locator + valid ECC mode + valid payload config + remote policy
var data = Data()
data.append(Header.magicNumber)
data.append(Header.versionV12)
data.append(ResourceLocator(protocolEnum: .http, body: "kas.example.com")!.toData())
data.append(PolicyBindingConfig(ecdsaBinding: false, curve: .secp256r1).toData())
data.append(SignatureAndPayloadConfig(signed: false, signatureCurve: nil, payloadCipher: .aes256GCM128).toData())
data.append(Policy.PolicyType.remote.rawValue)
data.append(ResourceLocator(protocolEnum: .http, body: "kas.example.com/policy")!.toData())
data.append(Data(count: 8)) // placeholder GMAC binding
// Intentionally omit the ephemeral public key

let parser = BinaryParser(data: data)
XCTAssertThrowsError(try parser.parseHeader()) { error in
guard let parsingError = error as? ParsingError,
case .invalidFormat = parsingError
else {
XCTFail("Expected ParsingError.invalidFormat, got \(error)")
return
}
}
}

func testMalformedResourceLocatorThrows() {
var data = Data()
data.append(Header.magicNumber)
data.append(Header.versionV12)
// Protocol byte with invalid protocol value (0xFF) and zero-length body
data.append(contentsOf: [0xFF, 0x00])

let parser = BinaryParser(data: data)
XCTAssertThrowsError(try parser.parseHeader()) { error in
guard let parsingError = error as? ParsingError,
case .invalidFormat = parsingError
else {
XCTFail("Expected ParsingError.invalidFormat, got \(error)")
return
}
}
}
}
90 changes: 0 additions & 90 deletions OpenTDFKitTests/CryptoHelperTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import CryptoKit
import Foundation
import XCTest

Expand Down Expand Up @@ -53,92 +52,3 @@ final class CryptoHelperTests: XCTestCase {
)
}
}

/// Add these to CryptoHelper.swift:
extension CryptoHelper {
/// Combined operation that keeps sensitive types within the actor
struct EncryptionResult {
let gmacTag: Data
let nonce: Data
let ciphertext: Data
let tag: Data
}

func deriveKeysAndEncrypt(
keyPair: EphemeralKeyPair,
recipientPublicKey: Data,
plaintext: Data,
policyBody: Data,
) throws -> EncryptionResult {
// Derive shared secret
guard let sharedSecret = try deriveSharedSecret(
keyPair: keyPair,
recipientPublicKey: recipientPublicKey,
) else {
throw CryptoHelperError.keyDerivationFailed
}

// Derive symmetric key
let symmetricKey = deriveSymmetricKey(
sharedSecret: sharedSecret,
salt: CryptoConstants.hkdfSalt,
info: CryptoConstants.hkdfInfoEncryption,
outputByteCount: CryptoConstants.symmetricKeyByteCount,
)

// Create GMAC binding
let gmacTag = try createGMACBinding(
policyBody: policyBody,
symmetricKey: symmetricKey,
)

// Generate nonce
let nonce = try generateNonce()

// Encrypt payload
let (ciphertext, tag) = try encryptPayload(
plaintext: plaintext,
symmetricKey: symmetricKey,
nonce: nonce,
)

return EncryptionResult(
gmacTag: gmacTag,
nonce: nonce,
ciphertext: ciphertext,
tag: tag,
)
}

func decryptWithDerivedKeys(
keyPair: EphemeralKeyPair,
recipientPublicKey: Data,
ciphertext: Data,
nonce: Data,
tag: Data,
) throws -> Data {
// Derive shared secret
guard let sharedSecret = try deriveSharedSecret(
keyPair: keyPair,
recipientPublicKey: recipientPublicKey,
) else {
throw CryptoHelperError.keyDerivationFailed
}

// Derive symmetric key
let symmetricKey = deriveSymmetricKey(
sharedSecret: sharedSecret,
salt: CryptoConstants.hkdfSalt,
info: CryptoConstants.hkdfInfoEncryption,
outputByteCount: CryptoConstants.symmetricKeyByteCount,
)

// Decrypt
return try decryptPayload(
ciphertext: ciphertext,
symmetricKey: symmetricKey,
nonce: nonce,
tag: tag,
)
}
}
91 changes: 91 additions & 0 deletions OpenTDFKitTests/Helpers/CryptoHelper+TestHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import CryptoKit
import Foundation
@testable import OpenTDFKit

extension CryptoHelper {
/// Combined operation that keeps sensitive types within the actor
struct EncryptionResult {
let gmacTag: Data
let nonce: Data
let ciphertext: Data
let tag: Data
}

func deriveKeysAndEncrypt(
keyPair: EphemeralKeyPair,
recipientPublicKey: Data,
plaintext: Data,
policyBody: Data,
) throws -> EncryptionResult {
// Derive shared secret
guard let sharedSecret = try deriveSharedSecret(
keyPair: keyPair,
recipientPublicKey: recipientPublicKey,
) else {
throw CryptoHelperError.keyDerivationFailed
}

// Derive symmetric key
let symmetricKey = deriveSymmetricKey(
sharedSecret: sharedSecret,
salt: CryptoConstants.hkdfSalt,
info: CryptoConstants.hkdfInfoEncryption,
outputByteCount: CryptoConstants.symmetricKeyByteCount,
)

// Create GMAC binding
let gmacTag = try createGMACBinding(
policyBody: policyBody,
symmetricKey: symmetricKey,
)

// Generate nonce
let nonce = try generateNonce()

// Encrypt payload
let (ciphertext, tag) = try encryptPayload(
plaintext: plaintext,
symmetricKey: symmetricKey,
nonce: nonce,
)

return EncryptionResult(
gmacTag: gmacTag,
nonce: nonce,
ciphertext: ciphertext,
tag: tag,
)
}

func decryptWithDerivedKeys(
keyPair: EphemeralKeyPair,
recipientPublicKey: Data,
ciphertext: Data,
nonce: Data,
tag: Data,
) throws -> Data {
// Derive shared secret
guard let sharedSecret = try deriveSharedSecret(
keyPair: keyPair,
recipientPublicKey: recipientPublicKey,
) else {
throw CryptoHelperError.keyDerivationFailed
}

// Derive symmetric key
let symmetricKey = deriveSymmetricKey(
sharedSecret: sharedSecret,
salt: CryptoConstants.hkdfSalt,
info: CryptoConstants.hkdfInfoEncryption,
outputByteCount: CryptoConstants.symmetricKeyByteCount,
)

// Decrypt
return try decryptPayload(
ciphertext: ciphertext,
symmetricKey: symmetricKey,
nonce: nonce,
tag: tag,
)
}
}
1 change: 0 additions & 1 deletion OpenTDFKitTests/InitializationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ final class InitializationTests: XCTestCase {
XCTAssertNotNil(locator)
let nanoTDF = initializeSmallNanoTDF(kasResourceLocator: locator!)
let data = nanoTDF.toData()
print("data.count", data.count)
XCTAssertLessThan(data.count, 240)
}

Expand Down
13 changes: 11 additions & 2 deletions OpenTDFKitTests/KASServiceBenchmarkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,17 @@ final class KASServiceBenchmarkTests: XCTestCase {
let policyData = Data(repeating: 0x42, count: size)
let symmetricKey = SymmetricKey(size: .bits256)

// Create a GMAC binding
let policyBinding = try AES.GCM.seal(Data(), using: symmetricKey, authenticating: policyData).tag
// Create a deterministic GMAC binding using the same zero nonce as production.
let nonce = try AES.GCM.Nonce(data: Data(count: 12))
let policyBinding = try AES.GCM.seal(Data(), using: symmetricKey, nonce: nonce, authenticating: policyData).tag

// Sanity-check that verification succeeds before benchmarking.
let isValid = try await kasService.verifyPolicyBinding(
policyBinding: policyBinding,
policyData: policyData,
symmetricKey: symmetricKey,
)
XCTAssertTrue(isValid, "Benchmark binding should be valid")

// Start benchmark
let startTime = DispatchTime.now()
Expand Down
Loading
Loading