Skip to content

Commit 29503ad

Browse files
committed
Testing Hashcash
1 parent 2f37ae0 commit 29503ad

3 files changed

Lines changed: 243 additions & 6 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//
2+
// Hashcash.swift
3+
//
4+
//
5+
// Created by Matt Kiazyk on 2023-02-23.
6+
//
7+
8+
import Foundation
9+
import CryptoKit
10+
import CommonCrypto
11+
12+
public struct Hashcash {
13+
14+
public func mint(resource: String,
15+
bits: UInt = 20,
16+
ext: String = "",
17+
saltCharacters: UInt = 16,
18+
stampSeconds: Bool = true,
19+
date: String? = nil) -> String? {
20+
21+
let ver = "1"
22+
23+
var ts: String
24+
if let date = date {
25+
ts = date
26+
} else {
27+
let formatter = DateFormatter()
28+
formatter.dateFormat = stampSeconds ? "yyMMddHHmmss" : "yyMMdd"
29+
ts = formatter.string(from: Date())
30+
}
31+
32+
let challenge = "\(ver):\(bits):\(ts):\(resource):"
33+
34+
var counter = 0
35+
let hexDigits = Int(ceil((Double(bits) / 4)))
36+
let zeros = String(repeating: "0", count: hexDigits)
37+
38+
while true {
39+
guard let digest = ("\(challenge):\(counter)").sha1 else {
40+
print("ERROR: Can't generate SHA1 digest")
41+
return nil
42+
}
43+
44+
if digest.prefix(hexDigits) == zeros {
45+
return "\(challenge):\(counter)"
46+
}
47+
counter += 1
48+
}
49+
}
50+
51+
/**
52+
Checks whether a stamp is valid
53+
- parameter stamp: stamp to check e.g. 1:16:040922:foo::+ArSrtKd:164b3
54+
- parameter resource: resource to check against
55+
- parameter bits: minimum bit value to check
56+
- parameter expiration: number of seconds old the stamp may be
57+
- returns: true if stamp is valid
58+
*/
59+
public func check(stamp: String,
60+
resource: String? = nil,
61+
bits: UInt,
62+
expiration: UInt? = nil) -> Bool {
63+
64+
guard let stamped = Stamp(stamp: stamp) else {
65+
print("Invalid stamp format")
66+
return false
67+
}
68+
69+
if let res = resource, res != stamped.resource {
70+
print("Resources do not match")
71+
return false
72+
}
73+
74+
var count = bits
75+
if let claim = stamped.claim {
76+
if bits > claim {
77+
return false
78+
} else {
79+
count = claim
80+
}
81+
}
82+
83+
if let expiration = expiration {
84+
let goodUntilDate = Date(timeIntervalSinceNow: -TimeInterval(expiration))
85+
if (stamped.date < goodUntilDate) {
86+
print("Stamp expired")
87+
return false
88+
}
89+
}
90+
91+
guard let digest = stamp.sha1 else {
92+
return false
93+
}
94+
95+
let hexDigits = Int(ceil((Double(count) / 4)))
96+
return digest.hasPrefix(String(repeating: "0", count: hexDigits))
97+
}
98+
99+
/**
100+
Generates random string of chosen length
101+
- parameter length: length of random string
102+
- returns: random string
103+
*/
104+
internal func salt(length: UInt) -> String {
105+
let allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/="
106+
var result = ""
107+
108+
for _ in 0..<length {
109+
let randomValue = arc4random_uniform(UInt32(allowedCharacters.count))
110+
result += "\(allowedCharacters[allowedCharacters.index(allowedCharacters.startIndex, offsetBy: Int(randomValue))])"
111+
}
112+
return result
113+
}
114+
}
115+
116+
extension String {
117+
var sha1: String? {
118+
let data = Data(self.utf8)
119+
var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH))
120+
data.withUnsafeBytes {
121+
_ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
122+
}
123+
let hexBytes = digest.map { String(format: "%02x", $0) }
124+
return hexBytes.joined()
125+
}
126+
}
127+
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// Stamp.swift
3+
//
4+
//
5+
// Created by Matt Kiazyk on 2023-02-23.
6+
//
7+
8+
import Foundation
9+
10+
public struct Stamp {
11+
12+
private static let DateFormatWithoutTime = "yyMMdd"
13+
private static let DateFormatWithTime = "yyMMddHHmmss"
14+
15+
public let version : UInt
16+
public let date : Date
17+
public let resource : String
18+
19+
// Version 1 only
20+
public var claim : UInt?
21+
public var counter : String?
22+
public var ext : String?
23+
public var random : String?
24+
25+
// Version 0 only
26+
public var suffix : String?
27+
28+
init?(stamp: String) {
29+
let components = stamp.components(separatedBy: ":")
30+
31+
if (components.count < 1) {
32+
print("No stamp components. Ensure it is separated by a `:`")
33+
return nil
34+
}
35+
36+
guard let version = UInt(components[0]) else {
37+
print("Unable to parse stamp version")
38+
return nil
39+
}
40+
41+
self.version = version
42+
43+
if self.version > 1 {
44+
print("Version > 1. Not handled")
45+
return nil
46+
}
47+
48+
if (self.version == 0 && components.count < 4) {
49+
print("Not enough components for version 0")
50+
return nil
51+
}
52+
53+
if (self.version == 1 && components.count < 7) {
54+
print("Not enough components for version 1")
55+
return nil
56+
}
57+
58+
if (self.version == 0) {
59+
if let date = Stamp.parseDate(dateString: components[1]) {
60+
self.date = date
61+
} else {
62+
return nil
63+
}
64+
self.resource = components[2]
65+
self.suffix = components[3]
66+
} else if (self.version == 1) {
67+
if let claim = UInt(components[1]) {
68+
self.claim = claim
69+
}
70+
if let date = Stamp.parseDate(dateString: components[2]) {
71+
self.date = date
72+
} else {
73+
return nil
74+
}
75+
self.resource = components[3]
76+
self.ext = components[4]
77+
self.random = components[5]
78+
self.counter = components[6]
79+
} else {
80+
return nil
81+
}
82+
}
83+
84+
private static func parseDate(dateString: String) -> Date? {
85+
let formatter = DateFormatter()
86+
formatter.dateFormat = Stamp.DateFormatWithoutTime
87+
88+
if let date = formatter.date(from: dateString) {
89+
return date
90+
}
91+
92+
formatter.dateFormat = Stamp.DateFormatWithTime
93+
94+
if let date = formatter.date(from: dateString) {
95+
return date
96+
} else {
97+
print("Unable to parse date")
98+
return nil
99+
}
100+
}
101+
}

Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@ import XCTest
22
@testable import AppleAPI
33

44
final class AppleAPITests: XCTestCase {
5-
func testExample() {
6-
// This is an example of a functional test case.
7-
// Use XCTAssert and related functions to verify your tests produce the correct
8-
// results.
9-
XCTAssertEqual(AppleAPI().text, "Hello, World!")
5+
6+
func testValidHashCashMint() {
7+
let bits: UInt = 10
8+
let resource = "bb63edf88d2f9c39f23eb4d6f0281158"
9+
let testDate = "20230224001754"
10+
11+
// "1:11:20230224004345:8982e236688f6ebf588c4bd4b445c4cc::877"
12+
// 7395f792caf430dca2d07ae7be0c63fa
13+
14+
let stamp = Hashcash().mint(resource: resource, bits: bits, date: testDate)
15+
XCTAssertNotNil(stamp)
16+
XCTAssertEqual(stamp, "1:10:20230224001754:bb63edf88d2f9c39f23eb4d6f0281158::866")
17+
18+
print(stamp)
1019
}
1120

1221
static var allTests = [
13-
("testExample", testExample),
22+
("testValidHashCashMint", testValidHashCashMint),
1423
]
1524
}

0 commit comments

Comments
 (0)