diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift new file mode 100644 index 0000000..42d8b39 --- /dev/null +++ b/Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift @@ -0,0 +1,127 @@ +// +// Hashcash.swift +// +// +// Created by Matt Kiazyk on 2023-02-23. +// + +import Foundation +import CryptoKit +import CommonCrypto + +public struct Hashcash { + + public func mint(resource: String, + bits: UInt = 20, + ext: String = "", + saltCharacters: UInt = 16, + stampSeconds: Bool = true, + date: String? = nil) -> String? { + + let ver = "1" + + var ts: String + if let date = date { + ts = date + } else { + let formatter = DateFormatter() + formatter.dateFormat = stampSeconds ? "yyMMddHHmmss" : "yyMMdd" + ts = formatter.string(from: Date()) + } + + let challenge = "\(ver):\(bits):\(ts):\(resource):" + + var counter = 0 + let hexDigits = Int(ceil((Double(bits) / 4))) + let zeros = String(repeating: "0", count: hexDigits) + + while true { + guard let digest = ("\(challenge):\(counter)").sha1 else { + print("ERROR: Can't generate SHA1 digest") + return nil + } + + if digest.prefix(hexDigits) == zeros { + return "\(challenge):\(counter)" + } + counter += 1 + } + } + + /** + Checks whether a stamp is valid + - parameter stamp: stamp to check e.g. 1:16:040922:foo::+ArSrtKd:164b3 + - parameter resource: resource to check against + - parameter bits: minimum bit value to check + - parameter expiration: number of seconds old the stamp may be + - returns: true if stamp is valid + */ + public func check(stamp: String, + resource: String? = nil, + bits: UInt, + expiration: UInt? = nil) -> Bool { + + guard let stamped = Stamp(stamp: stamp) else { + print("Invalid stamp format") + return false + } + + if let res = resource, res != stamped.resource { + print("Resources do not match") + return false + } + + var count = bits + if let claim = stamped.claim { + if bits > claim { + return false + } else { + count = claim + } + } + + if let expiration = expiration { + let goodUntilDate = Date(timeIntervalSinceNow: -TimeInterval(expiration)) + if (stamped.date < goodUntilDate) { + print("Stamp expired") + return false + } + } + + guard let digest = stamp.sha1 else { + return false + } + + let hexDigits = Int(ceil((Double(count) / 4))) + return digest.hasPrefix(String(repeating: "0", count: hexDigits)) + } + + /** + Generates random string of chosen length + - parameter length: length of random string + - returns: random string + */ + internal func salt(length: UInt) -> String { + let allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/=" + var result = "" + + for _ in 0.. 1 { + print("Version > 1. Not handled") + return nil + } + + if (self.version == 0 && components.count < 4) { + print("Not enough components for version 0") + return nil + } + + if (self.version == 1 && components.count < 7) { + print("Not enough components for version 1") + return nil + } + + if (self.version == 0) { + if let date = Stamp.parseDate(dateString: components[1]) { + self.date = date + } else { + return nil + } + self.resource = components[2] + self.suffix = components[3] + } else if (self.version == 1) { + if let claim = UInt(components[1]) { + self.claim = claim + } + if let date = Stamp.parseDate(dateString: components[2]) { + self.date = date + } else { + return nil + } + self.resource = components[3] + self.ext = components[4] + self.random = components[5] + self.counter = components[6] + } else { + return nil + } + } + + private static func parseDate(dateString: String) -> Date? { + let formatter = DateFormatter() + formatter.dateFormat = Stamp.DateFormatWithoutTime + + if let date = formatter.date(from: dateString) { + return date + } + + formatter.dateFormat = Stamp.DateFormatWithTime + + if let date = formatter.date(from: dateString) { + return date + } else { + print("Unable to parse date") + return nil + } + } +} diff --git a/Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift b/Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift index 283dbc6..b161151 100644 --- a/Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift +++ b/Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift @@ -2,14 +2,23 @@ import XCTest @testable import AppleAPI final class AppleAPITests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(AppleAPI().text, "Hello, World!") + + func testValidHashCashMint() { + let bits: UInt = 10 + let resource = "bb63edf88d2f9c39f23eb4d6f0281158" + let testDate = "20230224001754" + +// "1:11:20230224004345:8982e236688f6ebf588c4bd4b445c4cc::877" +// 7395f792caf430dca2d07ae7be0c63fa + + let stamp = Hashcash().mint(resource: resource, bits: bits, date: testDate) + XCTAssertNotNil(stamp) + XCTAssertEqual(stamp, "1:10:20230224001754:bb63edf88d2f9c39f23eb4d6f0281158::866") + + print(stamp) } static var allTests = [ - ("testExample", testExample), + ("testValidHashCashMint", testValidHashCashMint), ] }