Testing Hashcash

This commit is contained in:
Matt Kiazyk 2023-02-23 23:04:31 -06:00
parent 2f37ae0e41
commit 29503ad9cf
No known key found for this signature in database
GPG key ID: 850581D2373E4A99
3 changed files with 243 additions and 6 deletions

View file

@ -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..<length {
let randomValue = arc4random_uniform(UInt32(allowedCharacters.count))
result += "\(allowedCharacters[allowedCharacters.index(allowedCharacters.startIndex, offsetBy: Int(randomValue))])"
}
return result
}
}
extension String {
var sha1: String? {
let data = Data(self.utf8)
var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
}
let hexBytes = digest.map { String(format: "%02x", $0) }
return hexBytes.joined()
}
}

View file

@ -0,0 +1,101 @@
//
// Stamp.swift
//
//
// Created by Matt Kiazyk on 2023-02-23.
//
import Foundation
public struct Stamp {
private static let DateFormatWithoutTime = "yyMMdd"
private static let DateFormatWithTime = "yyMMddHHmmss"
public let version : UInt
public let date : Date
public let resource : String
// Version 1 only
public var claim : UInt?
public var counter : String?
public var ext : String?
public var random : String?
// Version 0 only
public var suffix : String?
init?(stamp: String) {
let components = stamp.components(separatedBy: ":")
if (components.count < 1) {
print("No stamp components. Ensure it is separated by a `:`")
return nil
}
guard let version = UInt(components[0]) else {
print("Unable to parse stamp version")
return nil
}
self.version = version
if self.version > 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
}
}
}

View file

@ -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),
]
}