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 4 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
9 changes: 9 additions & 0 deletions Sources/tbDEX/Protocol/CryptoUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,18 @@ extension CryptoUtils {
static func digest<D: Codable, M: Codable>(data: D, metadata: M) throws -> Data {
let payload = DigestPayload(data: data, metadata: metadata)
let serializedPayload = try tbDEXJSONEncoder().encode(payload)

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
}

/// Encapsulates data and metadata for digest computation.
private struct DigestPayload<D: Codable, M: Codable>: Codable {
Expand Down
20 changes: 12 additions & 8 deletions Sources/tbDEX/Protocol/DevTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,31 +80,35 @@ 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,
rfqData: rfqData,
kirahsapong marked this conversation as resolved.
Show resolved Hide resolved
externalID: externalID,
protocol: `protocol`
)
} else {
return RFQ(to: to, from: from, data: rfqData, externalID: externalID)
return try RFQ(
to: to,
from: from,
rfqData:
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
203 changes: 188 additions & 15 deletions Sources/tbDEX/Protocol/Models/Messages/RFQ.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ extension RFQ {
public init(
to: String,
from: String,
data: RFQData,
rfqData: CreateRFQData,
externalID: String? = nil,
`protocol`: String = "1.0"
) {
) throws {
let hashedData = try hashPrivateData(rfqData: rfqData)
self.data = hashedData["data"] as! RFQData
self.privateData = hashedData["privateData"] as? RFQPrivateData

let id = TypeID(prefix: data.kind().rawValue)!
self.metadata = MessageMetadata(
id: id,
Expand All @@ -25,10 +29,59 @@ extension RFQ {
externalID: externalID,
protocol: `protocol`
)
self.data = data
self.private = nil
}
}

private func generateSalt(_ count: Int) throws -> String? {
let randomBytes = [UInt8](repeating: UInt8.random(in: 0...255), count: count)
kirahsapong marked this conversation as resolved.
Show resolved Hide resolved

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

private func digestPrivateData(salt: String, value: Codable) throws -> String? {
do {
let encodedSalt = try tbDEXJSONEncoder().encode(salt)
let encodedData = try tbDEXJSONEncoder().encode(value)
let byteArray = try CryptoUtils.digestToByteArray(payload: [encodedSalt, encodedData])
return byteArray.base64UrlEncodedString()
} catch {
throw Error(reason: "Error digesting privateData: \(error)")
}
}

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

let data = RFQData(
offeringId: rfqData.offeringId,
payin: .init(
amount: rfqData.payin.amount,
kind: rfqData.payin.kind,
paymentDetailsHash: try digestPrivateData(salt: salt, value: rfqData.payin.paymentDetails)
),
payout: .init(
kind: rfqData.payout.kind,
paymentDetailsHash: try digestPrivateData(salt: salt, value: rfqData.payout.paymentDetails)
),
claimsHash: rfqData.claims?.isEmpty ?? (rfqData.claims == nil) ? nil :
try digestPrivateData(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]
}

/// Data that makes up a RFQ Message.
Expand All @@ -45,24 +98,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 +130,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 +152,134 @@ 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.payin.paymentDetails`
kirahsapong marked this conversation as resolved.
Show resolved Hide resolved
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

/// A salted hash of `privateData.payin.paymentDetails`
kirahsapong marked this conversation as resolved.
Show resolved Hide resolved
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

/// A salted hash of `privateData.payin.paymentDetails`
kirahsapong marked this conversation as resolved.
Show resolved Hide resolved
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 typealias RFQPrivateData = RFQUnhashedData
kirahsapong marked this conversation as resolved.
Show resolved Hide resolved

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