Skip to content

Commit

Permalink
Hash RFQ private data (#89)
Browse files Browse the repository at this point in the history
* bump spec

* add rfq private data field , add hashing

* update tests

* rename rfq fields and structs

* test hashed fields

* fix comments
  • Loading branch information
kirahsapong committed Apr 1, 2024
1 parent 9379ff6 commit 973c754
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 182 deletions.
13 changes: 13 additions & 0 deletions Sources/tbDEX/Protocol/CryptoUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ extension CryptoUtils {
let digest = SHA256.hash(data: serializedPayload)
return Data(digest)
}

static func digestToByteArray(payload: Codable) throws -> [UInt8] {
let serializedPayload = try tbDEXJSONEncoder().encode(payload)
let digest = SHA256.hash(data: serializedPayload)
return digest.bytes
}

static func digestRFQPrivateData(salt: String, value: Codable) throws -> String? {
let encodedSalt = try tbDEXJSONEncoder().encode(salt)
let encodedData = try tbDEXJSONEncoder().encode(value)
let byteArray = try CryptoUtils.digestToByteArray(payload: [encodedSalt, encodedData])
return byteArray.base64UrlEncodedString()
}

/// Encapsulates data and metadata for digest computation.
private struct DigestPayload<D: Codable, M: Codable>: Codable {
Expand Down
17 changes: 10 additions & 7 deletions Sources/tbDEX/Protocol/DevTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,31 +80,34 @@ enum DevTools {
from: String,
to: String,
externalID: String? = nil,
data: RFQData? = nil,
data: CreateRFQData? = nil,
protocol: String? = nil
) -> RFQ {
let rfqData = data ?? RFQData(
) throws -> RFQ {
let rfqData = data ?? CreateRFQData(
offeringId: TypeID(rawValue:"offering_01hmz7ehw6e5k9bavj0ywypfpy")!,
payin: .init(
amount: "1.00",
kind: "DEBIT_CARD"
),
payout: .init(
kind: "BITCOIN_ADDRESS"
),
claims: []
)
)

if let `protocol` = `protocol` {
return RFQ(
return try RFQ(
to: to,
from: from,
data: rfqData,
externalID: externalID,
protocol: `protocol`
)
} else {
return RFQ(to: to, from: from, data: rfqData, externalID: externalID)
return try RFQ(
to: to,
from: from,
data: rfqData,
externalID: externalID)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/tbDEX/Protocol/Models/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public struct Message<D: MessageData>: Codable, Equatable {
public private(set) var signature: String?

/// An ephemeral JSON object used to transmit sensitive data (e.g. PII)
public let `private`: AnyCodable?
public let privateData: RFQPrivateData?

/// Default Initializer. `protocol` defaults to "1.0" if nil
public init(
Expand All @@ -42,7 +42,7 @@ public struct Message<D: MessageData>: Codable, Equatable {
)
self.data = data
self.signature = nil
self.private = nil
self.privateData = nil
}

private func digest() throws -> Data {
Expand Down
201 changes: 184 additions & 17 deletions Sources/tbDEX/Protocol/Models/Messages/RFQ.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,71 @@ extension RFQ {
public init(
to: String,
from: String,
data: RFQData,
data: CreateRFQData,
externalID: String? = nil,
`protocol`: String = "1.0"
) {
let id = TypeID(prefix: data.kind().rawValue)!
) throws {
let hashedData = try hashPrivateData(rfqData: data)
self.data = hashedData["data"] as! RFQData
self.privateData = hashedData["privateData"] as? RFQPrivateData

let id = TypeID(prefix: self.data.kind().rawValue)!
self.metadata = MessageMetadata(
id: id,
kind: data.kind(),
kind: self.data.kind(),
from: from,
to: to,
exchangeID: id.rawValue,
createdAt: Date(),
externalID: externalID,
protocol: `protocol`
)
self.data = data
self.private = nil
}
}

private func generateSalt(_ count: Int) throws -> String? {
var randomBytes = [UInt8](repeating: 0, count: count)
_ = SecRandomCopyBytes(kSecRandomDefault, count, &randomBytes)

let encodedBytes = try tbDEXJSONEncoder().encode(randomBytes)
return encodedBytes.base64UrlEncodedString()
}

private func hashPrivateData(rfqData: CreateRFQData) throws -> [String: Any] {
guard let salt = try generateSalt(16) else {
throw Error(reason: "Failed to generate salt")
}

do {
let data = RFQData(
offeringId: rfqData.offeringId,
payin: .init(
amount: rfqData.payin.amount,
kind: rfqData.payin.kind,
paymentDetailsHash: try CryptoUtils.digestRFQPrivateData(salt: salt, value: rfqData.payin.paymentDetails)
),
payout: .init(
kind: rfqData.payout.kind,
paymentDetailsHash: try CryptoUtils.digestRFQPrivateData(salt: salt, value: rfqData.payout.paymentDetails)
),
claimsHash: rfqData.claims?.isEmpty ?? (rfqData.claims == nil) ? nil :
try CryptoUtils.digestRFQPrivateData(salt: salt, value: rfqData.claims)
)
let privateData = RFQPrivateData(
salt: salt,
payin: .init(
paymentDetails: rfqData.payin.paymentDetails
),
payout: .init(
paymentDetails: rfqData.payout.paymentDetails
),
claims: rfqData.claims
)

return ["data": data, "privateData": privateData]

} catch {
throw Error(reason: "Error digesting privateData: \(error)")
}

}
Expand All @@ -45,24 +93,24 @@ public struct RFQData: MessageData {
/// Details and options associated to the payout currency
public let payout: SelectedPayoutMethod

/// An array of claims that fulfill the requirements declared in an Offering.
public let claims: [String]
/// Salted hash of the claims appearing in `privateData.claims`
public let claimsHash: String?

/// Returns the MessageKind of rfq
public func kind() -> MessageKind {
return .rfq
}

public init(
offeringId: TypeID,
offeringId: String,
payin: SelectedPayinMethod,
payout: SelectedPayoutMethod,
claims: [String]
claimsHash: String? = nil
) {
self.offeringId = offeringId.rawValue
self.offeringId = offeringId
self.payin = payin
self.payout = payout
self.claims = claims
self.claimsHash = claimsHash
}
}

Expand All @@ -77,17 +125,17 @@ public struct SelectedPayinMethod: Codable, Equatable {
/// Type of payment method (i.e. `DEBIT_CARD`, `BITCOIN_ADDRESS`, `SQUARE_PAY`)
public let kind: String

/// An object containing the properties defined in an Offering's `requiredPaymentDetails` json schema
public let paymentDetails: AnyCodable?
/// A salted hash of `privateData.payin.paymentDetails`
public let paymentDetailsHash: String?

public init(
amount: String,
kind: String,
paymentDetails: AnyCodable? = nil
paymentDetailsHash: String? = nil
) {
self.amount = amount
self.kind = kind
self.paymentDetails = paymentDetails
self.paymentDetailsHash = paymentDetailsHash
}
}

Expand All @@ -99,14 +147,133 @@ public struct SelectedPayoutMethod: Codable, Equatable {
/// Type of payment method (i.e. `DEBIT_CARD`, `BITCOIN_ADDRESS`, `SQUARE_PAY`)
public let kind: String

/// An object containing the properties defined in an Offering's `requiredPaymentDetails` json schema
/// A salted hash of `privateData.payout.paymentDetails`
public let paymentDetailsHash: String?

public init(
kind: String,
paymentDetailsHash: String? = nil
) {
self.kind = kind
self.paymentDetailsHash = paymentDetailsHash
}
}

/// Data contained in a RFQ message, including data which will be placed in `RfqPrivateData`
public struct CreateRFQData: Codable, Equatable {

/// Offering which Alice would like to get a quote for.
public let offeringId: String

/// A container for the unhashed `payin.paymentDetails`
public let payin: CreateRFQPayinMethod

/// A container for the unhashed `payout.paymentDetails`
public let payout: CreateRFQPayoutMethod

/// An array of claims that fulfill the requirements declared in an Offering.
public let claims: [String]?

/// Default initializer
public init(
offeringId: TypeID,
payin: CreateRFQPayinMethod,
payout: CreateRFQPayoutMethod,
claims: [String]? = nil
) {
self.offeringId = offeringId.rawValue
self.payin = payin
self.payout = payout
self.claims = claims
}
}

public struct CreateRFQPayinMethod: Codable, Equatable {

/// Amount of payin currency you want in exchange for payout currency
public let amount: String

/// Type of payment method (i.e. `DEBIT_CARD`, `BITCOIN_ADDRESS`, `SQUARE_PAY`)
public let kind: String

/// An object containing the properties defined in an Offering's `payout.methods.requiredPaymentDetails` json schema
public let paymentDetails: AnyCodable?

public init(
amount: String,
kind: String,
paymentDetails: AnyCodable? = nil
) {
self.amount = amount
self.kind = kind
self.paymentDetails = paymentDetails
}
}

public struct CreateRFQPayoutMethod: Codable, Equatable {

/// Type of payment method (i.e. `DEBIT_CARD`, `BITCOIN_ADDRESS`, `SQUARE_PAY`)
public let kind: String

/// An object containing the properties defined in an Offering's `payout.methods.requiredPaymentDetails` json schema
public let paymentDetails: AnyCodable?

public init(
kind: String,
paymentDetails: AnyCodable? = nil
) {
self.kind = kind
self.paymentDetails = paymentDetails
}
}

/// Private data contained in a RFQ message

public struct RFQPrivateData: Codable, Equatable {
/// Randomly generated cryptographic salt used to hash `privateData` fields
public let salt: String

/// A container for the unhashed `payin.paymentDetails`
public let payin: PrivatePaymentDetails?

/// A container for the unhashed `payout.paymentDetails`
public let payout: PrivatePaymentDetails?

/// An array of claims that fulfill the requirements declared in an Offering.
public let claims: [String]?

public init(
salt: String,
payin: PrivatePaymentDetails? = nil,
payout: PrivatePaymentDetails? = nil,
claims: [String]? = nil
) {
self.salt = salt
self.payin = payin
self.payout = payout
self.claims = claims
}
}

/// A container for the unhashed `paymentDetails`
public struct PrivatePaymentDetails: Codable, Equatable {
/// An object containing the properties defined in an Offering's `requiredPaymentDetails` json schema
public let paymentDetails: AnyCodable?

public init(
paymentDetails: AnyCodable? = nil
) {
self.paymentDetails = paymentDetails
}
}

// MARK: - Errors

private struct Error: LocalizedError {
let reason: String

public var errorDescription: String? {
return reason
}
}

16 changes: 15 additions & 1 deletion Tests/tbDEXTestVectors/tbDEXTestVectorsProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ final class tbDEXTestVectorsProtocol: XCTestCase {
XCTAssertNoDifference(parsedQuote, vector.output)
}

func _test_parseRfq() throws {
func test_parseRfq() throws {
let vector = try TestVector<String, RFQ>(
fileName: "parse-rfq",
subdirectory: vectorSubdirectory
Expand All @@ -108,4 +108,18 @@ final class tbDEXTestVectorsProtocol: XCTestCase {

XCTAssertNoDifference(parsedRFQ, vector.output)
}

func test_parseRfqOmitPrivateData() throws {
let vector = try TestVector<String, RFQ>(
fileName: "parse-rfq-omit-private-data",
subdirectory: vectorSubdirectory
)

let parsedMessage = try AnyMessage.parse(vector.input)
guard case let .rfq(parsedRFQ) = parsedMessage else {
return XCTFail("Parsed message is not an RFQ")
}

XCTAssertNoDifference(parsedRFQ, vector.output)
}
}

0 comments on commit 973c754

Please sign in to comment.