@@ -9,13 +9,44 @@ import Foundation
99import CryptoKit
1010import CommonCrypto
1111
12+ /*
13+ # This App Store Connect hashcash spec was generously donated by...
14+ #
15+ # __ _
16+ # __ _ _ __ _ __ / _|(_) __ _ _ _ _ __ ___ ___
17+ # / _` || '_ \ | '_ \ | |_ | | / _` || | | || '__|/ _ \/ __|
18+ # | (_| || |_) || |_) || _|| || (_| || |_| || | | __/\__ \
19+ # \__,_|| .__/ | .__/ |_| |_| \__, | \__,_||_| \___||___/
20+ # |_| |_| |___/
21+ #
22+ #
23+ */
1224public struct Hashcash {
13-
25+ /// A function to returned a minted hash, using a bit and resource string
26+ ///
27+ /**
28+ X-APPLE-HC: 1:11:20230223170600:4d74fb15eb23f465f1f6fcbf534e5877::6373
29+ ^ ^ ^ ^ ^
30+ | | | | +-- Counter
31+ | | | +-- Resource
32+ | | +-- Date YYMMDD[hhmm[ss]]
33+ | +-- Bits (number of leading zeros)
34+ +-- Version
35+
36+ We can't use an off-the-shelf Hashcash because Apple's implementation is not quite the same as the spec/convention.
37+ 1. The spec calls for a nonce called "Rand" to be inserted between the Ext and Counter. They don't do that at all.
38+ 2. The Counter conventionally encoded as base-64 but Apple just uses the decimal number's string representation.
39+
40+ Iterate from Counter=0 to Counter=N finding an N that makes the SHA1(X-APPLE-HC) lead with Bits leading zero bits
41+ We get the "Resource" from the X-Apple-HC-Challenge header and Bits from X-Apple-HC-Bits
42+ */
43+ /// - Parameters:
44+ /// - resource: a string to be used for minting
45+ /// - bits: grabbed from `X-Apple-HC-Bits` header
46+ /// - date: Default uses Date() otherwise used for testing to check.
47+ /// - Returns: A String hash to use in `X-Apple-HC` header on /signin
1448 public func mint( resource: String ,
15- bits: UInt = 20 ,
16- ext: String = " " ,
17- saltCharacters: UInt = 16 ,
18- stampSeconds: Bool = true ,
49+ bits: UInt = 10 ,
1950 date: String ? = nil ) -> String ? {
2051
2152 let ver = " 1 "
@@ -25,103 +56,41 @@ public struct Hashcash {
2556 ts = date
2657 } else {
2758 let formatter = DateFormatter ( )
28- formatter. dateFormat = stampSeconds ? " yyMMddHHmmss " : " yyMMdd "
59+ formatter. dateFormat = " yyMMddHHmmss "
2960 ts = formatter. string ( from: Date ( ) )
3061 }
3162
3263 let challenge = " \( ver) : \( bits) : \( ts) : \( resource) : "
3364
3465 var counter = 0
35- let hexDigits = Int ( ceil ( ( Double ( bits) / 4 ) ) )
36- let zeros = String ( repeating: " 0 " , count: hexDigits)
3766
3867 while true {
3968 guard let digest = ( " \( challenge) : \( counter) " ) . sha1 else {
4069 print ( " ERROR: Can't generate SHA1 digest " )
4170 return nil
4271 }
4372
44- if digest. prefix ( hexDigits ) == zeros {
73+ if digest == bits {
4574 return " \( challenge) : \( counter) "
4675 }
4776 counter += 1
4877 }
4978 }
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- }
11479}
11580
11681extension String {
117- var sha1 : String ? {
82+ var sha1 : Int ? {
83+
11884 let data = Data ( self . utf8)
11985 var digest = [ UInt8] ( repeating: 0 , count: Int ( CC_SHA1_DIGEST_LENGTH) )
12086 data. withUnsafeBytes {
12187 _ = CC_SHA1 ( $0. baseAddress, CC_LONG ( data. count) , & digest)
12288 }
123- let hexBytes = digest. map { String ( format: " %02x " , $0) }
124- return hexBytes. joined ( )
89+ let bigEndianValue = digest. withUnsafeBufferPointer {
90+ ( $0. baseAddress!. withMemoryRebound ( to: UInt32 . self, capacity: 1 ) { $0 } )
91+ } . pointee
92+ let value = UInt32 ( bigEndian: bigEndianValue)
93+ return value. leadingZeroBitCount
12594 }
12695}
12796
0 commit comments