mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-26 09:05:46 +00:00
96 lines
3.3 KiB
Swift
96 lines
3.3 KiB
Swift
//
|
|
// Hashcash.swift
|
|
//
|
|
//
|
|
// Created by Matt Kiazyk on 2023-02-23.
|
|
//
|
|
|
|
import Foundation
|
|
import CryptoKit
|
|
import CommonCrypto
|
|
|
|
/*
|
|
# This App Store Connect hashcash spec was generously donated by...
|
|
#
|
|
# __ _
|
|
# __ _ _ __ _ __ / _|(_) __ _ _ _ _ __ ___ ___
|
|
# / _` || '_ \ | '_ \ | |_ | | / _` || | | || '__|/ _ \/ __|
|
|
# | (_| || |_) || |_) || _|| || (_| || |_| || | | __/\__ \
|
|
# \__,_|| .__/ | .__/ |_| |_| \__, | \__,_||_| \___||___/
|
|
# |_| |_| |___/
|
|
#
|
|
#
|
|
*/
|
|
public struct Hashcash {
|
|
/// A function to returned a minted hash, using a bit and resource string
|
|
///
|
|
/**
|
|
X-APPLE-HC: 1:11:20230223170600:4d74fb15eb23f465f1f6fcbf534e5877::6373
|
|
^ ^ ^ ^ ^
|
|
| | | | +-- Counter
|
|
| | | +-- Resource
|
|
| | +-- Date YYMMDD[hhmm[ss]]
|
|
| +-- Bits (number of leading zeros)
|
|
+-- Version
|
|
|
|
We can't use an off-the-shelf Hashcash because Apple's implementation is not quite the same as the spec/convention.
|
|
1. The spec calls for a nonce called "Rand" to be inserted between the Ext and Counter. They don't do that at all.
|
|
2. The Counter conventionally encoded as base-64 but Apple just uses the decimal number's string representation.
|
|
|
|
Iterate from Counter=0 to Counter=N finding an N that makes the SHA1(X-APPLE-HC) lead with Bits leading zero bits
|
|
We get the "Resource" from the X-Apple-HC-Challenge header and Bits from X-Apple-HC-Bits
|
|
*/
|
|
/// - Parameters:
|
|
/// - resource: a string to be used for minting
|
|
/// - bits: grabbed from `X-Apple-HC-Bits` header
|
|
/// - date: Default uses Date() otherwise used for testing to check.
|
|
/// - Returns: A String hash to use in `X-Apple-HC` header on /signin
|
|
public func mint(resource: String,
|
|
bits: UInt = 10,
|
|
date: String? = nil) -> String? {
|
|
|
|
let ver = "1"
|
|
|
|
var ts: String
|
|
if let date = date {
|
|
ts = date
|
|
} else {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyMMddHHmmss"
|
|
ts = formatter.string(from: Date())
|
|
}
|
|
|
|
let challenge = "\(ver):\(bits):\(ts):\(resource):"
|
|
|
|
var counter = 0
|
|
|
|
while true {
|
|
guard let digest = ("\(challenge):\(counter)").sha1 else {
|
|
print("ERROR: Can't generate SHA1 digest")
|
|
return nil
|
|
}
|
|
|
|
if digest == bits {
|
|
return "\(challenge):\(counter)"
|
|
}
|
|
counter += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
extension String {
|
|
var sha1: Int? {
|
|
|
|
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 bigEndianValue = digest.withUnsafeBufferPointer {
|
|
($0.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: 1) { $0 })
|
|
}.pointee
|
|
let value = UInt32(bigEndian: bigEndianValue)
|
|
return value.leadingZeroBitCount
|
|
}
|
|
}
|
|
|