CryptoKit TOTP Generation

Hi


I'm using the new CryptoKit to generate a 6 or 8 digit TOTP code. Anyone been successful doing this?


Using Xcode 11 BETA 5, targeting iOS 13 and Swift 5.1. Here is a snippet of generating an TOTP via CommonCrypto versus CryptoKit in playground (BETA). The base32Decode function returns Data.


import CryptoKit
import CommonCrypto
import Foundation

let period = TimeInterval(30)
let digits = 6
let secret = base32Decode(value: "5FAA5JZ7WHO5WDNN")!
var counter = UInt64(Date().timeIntervalSince1970 / period).bigEndian

func cryptoKitOTP() {
    // Generate the key based on the counter.
    let key = SymmetricKey(data: Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)))
    let hash = HMAC<Insecure.SHA1>.authenticationCode(for: secret, using: key)

    var truncatedHash = hash.withUnsafeBytes { ptr -> UInt32 in
        let offset = ptr[hash.byteCount - 1] & 0x0f

        let truncatedHashPtr = ptr.baseAddress! + Int(offset)
        return truncatedHashPtr.bindMemory(to: UInt32.self, capacity: 1).pointee
    }

    truncatedHash = UInt32(bigEndian: truncatedHash)
    truncatedHash = truncatedHash & 0x7FFF_FFFF
    truncatedHash = truncatedHash % UInt32(pow(10, Float(digits)))

    print("CryptoKit OTP value: \(String(format: "%0*u", digits, truncatedHash))")
}

func commonCryptoOTP() {
    let key = Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter))
    let (hashAlgorithm, hashLength) = (CCHmacAlgorithm(kCCHmacAlgSHA1), Int(CC_SHA1_DIGEST_LENGTH))

    let hashPtr = UnsafeMutablePointer.allocate(capacity: Int(hashLength))
    defer {
        hashPtr.deallocate()
    }

    secret.withUnsafeBytes { secretBytes in
        // Generate the key from the counter value.
        counterData.withUnsafeBytes { counterBytes in
            CCHmac(hashAlgorithm, secretBytes.baseAddress, secret.count, counterBytes.baseAddress, key.count, hashPtr)
        }
    }

    let hash = Data(bytes: hashPtr, count: Int(hashLength))
    var truncatedHash = hash.withUnsafeBytes { ptr -> UInt32 in
        let offset = ptr[hash.count - 1] & 0x0F
        let truncatedHashPtr = ptr.baseAddress! + Int(offset)
        return truncatedHashPtr.bindMemory(to: UInt32.self, capacity: 1).pointee
    }
    
    truncatedHash = UInt32(bigEndian: truncatedHash)
    truncatedHash = truncatedHash & 0x7FFF_FFFF
    truncatedHash = truncatedHash % UInt32(pow(10, Float(digits)))

    print("CommonCrypto OTP value: \(String(format: "%0*u", digits, truncatedHash))")
}

func otp() {
    commonCryptoOTP()
    cryptoKitOTP()
}

otp()

The output based on now as in 2:28pm is:

CommonCrypto OTP value: 819944

CryptoKit OTP value: 745890

To confirm the OTP value, I used oathtool which you can brew install to generate an array of TOTP's. For example:


oathtool --totp --base32 5FAA5JZ7WHO5WDNN -w 10


Craig

Answered by Security Engineer in 376829022

Hello Craig,

The code was almost right, it seems you just swapped the key and data field from the TOTP spec.


Here's the fixed code:

  • I changed the serialization of the key to base64 above as I was missing your dependency for base32.
  • I swapped the inputs to the HMAC code and adjusted to get the right types for each of the inputs.


Hope this fixes it!


import CryptoKit
import Foundation
 
let period = TimeInterval(30)
let digits = 6
let secret = Data(base64Encoded: "6UAOpz+x3dsNrQ==")!
var counter = UInt64(Date().timeIntervalSince1970 / period).bigEndian

func cryptoKitOTPFixed() {
    let counterData = withUnsafeBytes(of: &counter) { Array($0) }
    let hash = HMAC<Insecure.SHA1>.authenticationCode(for: counterData, using: SymmetricKey(data: secret))
 
    var truncatedHash = hash.withUnsafeBytes { ptr -> UInt32 in
        let offset = ptr[hash.byteCount - 1] & 0x0f
 
        let truncatedHashPtr = ptr.baseAddress! + Int(offset)
        return truncatedHashPtr.bindMemory(to: UInt32.self, capacity: 1).pointee
    }
 
    truncatedHash = UInt32(bigEndian: truncatedHash)
    truncatedHash = truncatedHash & 0x7FFF_FFFF
    truncatedHash = truncatedHash % UInt32(pow(10, Float(digits)))
 
    print("CryptoKitFixed OTP value: \(String(format: "%0*u", digits, truncatedHash))")
}

I tried running your code but it failed to compile because, on line 13, the compiler is unable to infer generic parameter

H
, that is, the hash function used by the HMAC. How did you get around that?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hey Quinn


It was a cut and paste, then edit problem, I've updated my snippet on line 13. Needed to read:

let hash = HMAC<Insecure.SHA1>.authenticationCode(for: secret, using: key)


Thanks for looking into this for me.


Craig

Hey Quinn


Did you mange to re-try the HMAC generation after I made the update?


Kind regards


Craig

Accepted Answer

Hello Craig,

The code was almost right, it seems you just swapped the key and data field from the TOTP spec.


Here's the fixed code:

  • I changed the serialization of the key to base64 above as I was missing your dependency for base32.
  • I swapped the inputs to the HMAC code and adjusted to get the right types for each of the inputs.


Hope this fixes it!


import CryptoKit
import Foundation
 
let period = TimeInterval(30)
let digits = 6
let secret = Data(base64Encoded: "6UAOpz+x3dsNrQ==")!
var counter = UInt64(Date().timeIntervalSince1970 / period).bigEndian

func cryptoKitOTPFixed() {
    let counterData = withUnsafeBytes(of: &counter) { Array($0) }
    let hash = HMAC<Insecure.SHA1>.authenticationCode(for: counterData, using: SymmetricKey(data: secret))
 
    var truncatedHash = hash.withUnsafeBytes { ptr -> UInt32 in
        let offset = ptr[hash.byteCount - 1] & 0x0f
 
        let truncatedHashPtr = ptr.baseAddress! + Int(offset)
        return truncatedHashPtr.bindMemory(to: UInt32.self, capacity: 1).pointee
    }
 
    truncatedHash = UInt32(bigEndian: truncatedHash)
    truncatedHash = truncatedHash & 0x7FFF_FFFF
    truncatedHash = truncatedHash % UInt32(pow(10, Float(digits)))
 
    print("CryptoKitFixed OTP value: \(String(format: "%0*u", digits, truncatedHash))")
}

Just had to specify the algorithm in line 11. Should be:

let hash = HMAC<Insecure.SHA1>.authenticationCode(for: counterData, using: SymmetricKey(data: secret))


Can confirm that I get the same OTP code generated with CryptoKit and CommonCrypto


Craig

Hi there, I see the last time this was updated was 2 years ago but things seem to be broken now.

The error that the compiler shows:

Cannot specialize a non-generic definition

  1. While parsing this '<' as a type parameter bracket

I have not seen too much of any answers around to resolve this. Thanks in advance and I appreciate the help.

Can you describe what the string format "%0*u" does?

This is a printf-style format modifier. For all the details, see the printf man page (in section 3). In this specific case the * and digits parameters left pad the value with leading zeros. For example, if truncatedHash is 1234, you get this:

CryptoKitFixed OTP value: 001234

whereas the more obvious code:

print("CryptoKitFixed OTP value: \(String(format: "%u", truncatedHash))")

prints this:

CryptoKitFixed OTP value: 1234

And is it possible to get an alpha-numeric code from this function?

This code implements TOTP, which is always numeric. If you want an alphanumeric code, you’re implementing some other standard, and so it’s hard to answer that without you telling us what standard you’re implementing.

ps IMO the code snippet above is less than ideal for a variety of reasons, not least of which is that it uses unsafe constructs when it doesn’t need to. Here’s a better alternative:

func otpCode(secret: Data, date: Date, period: TimeInterval, digits: Int) -> String {
    let counter = UInt64(date.timeIntervalSince1970 / period)
    let counterBytes = (0..<8).reversed().map { UInt8(counter >> (8 * $0) & 0xff) }
    let hash = HMAC<Insecure.SHA1>.authenticationCode(for: counterBytes, using: SymmetricKey(data: secret))
    let offset = Int(hash.suffix(1)[0] & 0x0f)
    let hash32 = hash
        .dropFirst(offset)
        .prefix(4)
        .reduce(0, { ($0 << 8) | UInt32($1) })
    let hash31 = hash32 & 0x7FFF_FFFF
    let pad = String(repeating: "0", count: digits)
    return String((pad + String(hash31)).suffix(digits))
}

The only tricky stuff here is this:

(0..<8).reversed().map { UInt8(counter >> (8 * $0) & 0xff) }

which converts counter, a UInt64, into a big-endian array of 8 bytes, and this:

.reduce(0, { ($0 << 8) | UInt32($1) })

which converts a sequence of 4 bytes into a UInt32, assuming big-endian.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

CryptoKit TOTP Generation
 
 
Q