Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Hash RFQ private data #89

Merged
merged 6 commits into from
Apr 1, 2024
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
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 {
kirahsapong marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
Loading
Loading