Search code examples
swiftencryptionhmacshaapple-cryptokit

How to get the same HMAC-SHA512 signature in Swift as in Python?


My goal is to interact with the (authenticated) Kraken API using Swift. On the Kraken API website is an example using Python and I’m trying to replicate that in Swift. However, the final encrypted signature that I’m getting is different from the one in Python. What am I doing wrong? Any help would be much appreciated. I'm using the Swift CryptoKit library by the way.

Python code (copied from the Kraken API documentation):

import urllib.parse
import hashlib
import hmac
import base64

def get_kraken_signature(urlpath, data, secret):

    postdata = urllib.parse.urlencode(data)
    encoded = (str(data['nonce']) + postdata).encode()
    message = urlpath.encode() + hashlib.sha256(encoded).digest()

    mac = hmac.new(base64.b64decode(secret), message, hashlib.sha512)
    sigdigest = base64.b64encode(mac.digest())
    return sigdigest.decode()

api_sec = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg=="

data = {
    "nonce": "1616492376594",
    "ordertype": "limit",
    "pair": "XBTUSD",
    "price": 37500,
    "type": "buy",
    "volume": 1.25
}

signature = get_kraken_signature("/0/private/AddOrder", data, api_sec)
print("API-Sign: {}".format(signature))
#Output: 4/dpxb3iT4tp/ZCVEwSnEsLxx0bqyhLpdfOpc6fn7OR8+UClSV5n9E6aSS8MPtnRfp32bAb0nmbRn6H8ndwLUQ==
#This output is the expected output, as Kraken states on their website

My Swift code:

import CryptoKit
func encryptMsg() -> String {
    let privateKey = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg=="
    let payload = "1616492376594nonce=1616492376594&ordertype=limit&pair=XBTUSD&price=37500&type=buy&volume=1.25"
    let apiPath = "/0/private/AddOrder"
    var payload2 = apiPath
    
    // Sha256Digest
    let sha256Digest: SHA256Digest = SHA256.hash(data: Data(payload.utf8))
    debugPrint(sha256Digest)
    // Output: 23a1c1b34c6a11d641af0f24684896cb90f66fb991125c83dc357bdc3dc146f1
    
    // Convert sha256Digest from hex to ascii
    for element in sha256Digest {
        payload2.append(Character(UnicodeScalar(Int(element.description)!)!))
    }
    debugPrint(payload2)
    // Swift : /0/private/AddOrder#¡Á³Lj\u{11}ÖA¯\u{0F}$hHËöo¹\u{12}\\Ü5{Ü=ÁFñ
    // Python: /0/private/AddOrder#\xa1\xc1\xb3Lj\x11\xd6A\xaf\x0f$hH\x96\xcb\x90\xf6o\xb9\x91\x12\\\x83\xdc5{\xdc=\xc1F\xf1
    // Swift output identical to Python output
    
    // Private key
    debugPrint(Data(base64Encoded: privateKey)!.map { String(format: "%02x", $0) }.joined())
    // Swift : 9101f91d6ffca75b863958db81603b16e9c09863bc96c4945cdb2eddea30efab33f38435f1f5b19f247304709dde97799c4f6a6bdf47019b6e66e8fa17586e5e
    // Python: \x91\x01\xf9\x1do\xfc\xa7[\x869X\xdb\x81`;\x16\xe9\xc0\x98c\xbc\x96\xc4\x94\\\xdb.\xdd\xea0\xef\xab3\xf3\x845\xf1\xf5\xb1\x9f$s\x04p\x9d\xde\x97y\x9cOjk\xdfG\x01\x9bnf\xe8\xfa\x17Xn^
    // Swift output identical to Python output
    let key = SymmetricKey(data: Data(base64Encoded: privateKey)!)
    
    // SHA512 digest and Hmac construction
    var hmacSHA512 = HMAC<SHA512>.authenticationCode(for: Data(payload2.utf8), using: key)
    debugPrint(hmacSHA512)
    // Swift : e82d03bb76af28c9bb55dfc460bf8a0f64d30d0e55307bc5236f5d0581bf8864bce237238d4b7cc3c832a7e2545b1d0f70794545ce76d1bd656c13b054ea54da
    // Python: \xe3\xf7i\xc5\xbd\xe2O\x8bi\xfd\x90\x95\x13\x04\xa7\x12\xc2\xf1\xc7F\xea\xca\x12\xe9u\xf3\xa9s\xa7\xe7\xec\xe4|\xf9@\xa5I^g\xf4N\x9aI/\x0c>\xd9\xd1~\x9d\xf6l\x06\xf4\x9ef\xd1\x9f\xa1\xfc\x9d\xdc\x0bQ
    // Swift output different from Python output
    
    let finalSignature = Data(hmacSHA512).base64EncodedString()
    debugPrint(finalSignature)
    // Swift : 6C0Du3avKMm7Vd/EYL+KD2TTDQ5VMHvFI29dBYG/iGS84jcjjUt8w8gyp+JUWx0PcHlFRc520b1lbBOwVOpU2g==
    // Python: 4/dpxb3iT4tp/ZCVEwSnEsLxx0bqyhLpdfOpc6fn7OR8+UClSV5n9E6aSS8MPtnRfp32bAb0nmbRn6H8ndwLUQ==
    // Final result: Swift output different from Python output
    
    return finalSignature
}

Solution

  • In your current approach, the components of the message are concatenated as strings. For this, the SHA256 hash is decoded into a string using Latin1. Therefore, for consistency reasons, a Latin1 encoding must also be used when generating the HMAC in order to reconstruct the original data.
    However, the current solution uses a UTF-8 encoding, which irreversibly corrupts the data. If the UTF-8 encoding is changed to a Latin1 encoding, the Swift code returns the result of the Python code:

    import Foundation
    import Crypto
    
    let privateKey = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg=="
    let payload = "1616492376594nonce=1616492376594&ordertype=limit&pair=XBTUSD&price=37500&type=buy&volume=1.25"
    let apiPath = "/0/private/AddOrder"
    let payload2 = apiPath
    
    var message = payload2    
    let sha256Digest: SHA256Digest = SHA256.hash(data: Data(payload.utf8))
    for element in sha256Digest {
        message.append(Character(UnicodeScalar(Int(element.description)!)!))
    }
    let key = SymmetricKey(data: Data(base64Encoded: privateKey)!)
    var hmacSHA512 = HMAC<SHA512>.authenticationCode(for: message.data(using: .isoLatin1)!, using: key) // fix: replace utf-8 with latin1
    let finalSignature = Data(hmacSHA512).base64EncodedString()
    
    print(finalSignature) // 4/dpxb3iT4tp/ZCVEwSnEsLxx0bqyhLpdfOpc6fn7OR8+UClSV5n9E6aSS8MPtnRfp32bAb0nmbRn6H8ndwLUQ==
    

    An alternative to your approach is to forego the decoding and encoding carried out solely for concatenation in favor of a direct concatenation of the binary data, e.g.:

    import Foundation
    import Crypto
    
    let privateKey = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg=="
    let payload = "1616492376594nonce=1616492376594&ordertype=limit&pair=XBTUSD&price=37500&type=buy&volume=1.25"
    let apiPath = "/0/private/AddOrder"
    let payload2 = apiPath
    
    var message: [UInt8] = Array(payload2.utf8)
    let sha256Digest: SHA256Digest = SHA256.hash(data: Data(payload.utf8))
    sha256Digest.withUnsafeBytes {message.append(contentsOf: $0)}
    let key = SymmetricKey(data: Data(base64Encoded: privateKey)!)
    var hmacSHA512 = HMAC<SHA512>.authenticationCode(for: message, using: key)
    let finalSignature = Data(hmacSHA512).base64EncodedString()
    
    print(finalSignature) // 4/dpxb3iT4tp/ZCVEwSnEsLxx0bqyhLpdfOpc6fn7OR8+UClSV5n9E6aSS8MPtnRfp32bAb0nmbRn6H8ndwLUQ==
    

    which of course also returns the result of the Python code.