Conditionally use CryptoKit

Hello, I'm trying to implement an SHA1 extension that uses CryptoKit if it is available, otherwise falls back to CommonCrypto. This is what I've tried:

#if canImport(CryptoKit)
import CryptoKit

extension Data {
    func sha1() -> Data {
        // Use Insecure.SHA1
    }
}
#else
import CommonCrypto

extension Data {
    func sha1() -> Data {
    	// Use CC_SHA1
    }
}
#endif

This doesn't seem to work though, I get errors that Insecure.SHA1 is only available in iOS 13. I've tried other combinations of #available and @available, but I can't seem to find the right way to do this.

1 Like

canImport just tells you whether the Framework is in the SDK, which it is because you’re using the iOS 13 SDK. Whether it’s available to use depends on what OS you’re running on, and that typically means a runtime check with #available.

When confronted with these problems I generally just use the API, compile, and then choose the right fixit (gosh I’m lazy!). For example, with this line:

extension Data {
    func sha1() -> Data {
        return Data(CryptoKit.Insecure.SHA1.hash(data: self))
    }
}

Xcode gives you fixits for this:

extension Data {
    func sha1() -> Data {
        if #available(iOS 13.0, *) {
            return Data(CryptoKit.Insecure.SHA1.hash(data: self))
        } else {
            // Fallback on earlier versions
        }
    }
}

and this:

extension Data {
    @available(iOS 13.0, *)
    func sha1() -> Data {
        return Data(CryptoKit.Insecure.SHA1.hash(data: self))
    }
}

and this:

@available(iOS 13.0, *)
extension Data {
    func sha1() -> Data {
        return Data(CryptoKit.Insecure.SHA1.hash(data: self))
    }
}

and it’s seems likely you want the first one.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes

This is possible at runtime, but not at compile time. i.e. CommonCrypto and its client implementation need to still be present on ≥ iOS 13, even though they are never used. (Or else you have to abandon everything < iOS 13.)

#if canImport(CryptoKit)
import CryptoKit
#endif
import CommonCrypto

extension Data {
    func sha1() -> Data {
        #if canImport(CryptoKit)
            if #available(iOS 13, *) {
                // ≥ iOS 13, macOS, etc.
                // Use Insecure.SHA1
                return ... 
            } else {
                // < iOS 13
                return commonCryptoSHA1()
            }
        #else
            // Linux, etc. (and iOS when compiled with < Xcode 11.)
            return commonCryptoSHA1()
        #endif
    }
    private func commonCryptoSHA1() -> Data {
        // Use CC_SHA1
    }
}
1 Like

What about the fallback implementation though? If I add a second extension with the same name (and without the @available) I get Invalid redeclaration of 'sha1()'.

Also what about the imports? Should the 2 modules be imported conditionally as well?

Thanks, that works, but is horribly nested. Ideally I would like to have 2 separate extensions, one using the new framework, the other having a fallback implementation. Possibly even in two different files.

The sha1 function is just an example, I have more complex use cases that would get very unwieldy if I was to implement them as one function.

Based on Jeremy's answer I found this solution that also works and is a bit more compartmenalized.

extension Data {
    func sha1() -> Data {
        #if canImport(CryptoKit)
        if #available(iOS 13, *) {
            return self.sha1_iOS13()
        } else {
            fatalError()
        }
        #else
        return self.sha1_legacy()
        #endif
    }
}

#if canImport(CryptoKit)
import CryptoKit

@available(iOS 13.0, *)
private extension Data {
    func sha1_iOS13() -> Data {
        return Data(Insecure.SHA1.hash(data: self))
    }
}
#else
import CommonCrypto

private extension Data {
    func sha1_legacy() -> Data {
    	return ...
    }
}
#endif

I'm wondering if there is a better solution with less boilerplate.

The #canImport() checks only matter at compile time, so they're really only checking "Are we building with Xcode 11 or later". I assume that you are building with Xcode 11, so you don't need them. As structured, your code will always get into the fatalError() branch on iOS 12 and earlier, if built with Xcode 11.

With that in mind, we can simplify your code to this:

import CryptoKit
import CommonCrypto

extension Data {
    func sha1() -> Data {
        if #available(iOS 13, *) {
            return self.sha1_iOS13()
        } else {
            return self.sha1_legacy()
        }
    }
}

@available(iOS 13.0, *)
private extension Data {
    func sha1_iOS13() -> Data {
        return Data(Insecure.SHA1.hash(data: self))
    }
}

private extension Data {
    func sha1_legacy() -> Data {
    	return ...
    }
}
2 Likes

If you want to support all platforms, then there is nothing shorter. But if you only support a subset of platforms anyway, then some of the checks may be superfluous:

That will always fatalError() on < iOS 13. If you do not actually plan to support < iOS 13, you can simply set it as your minimum deployment target (in SwiftPM or Xcode). Then you can drop all the @available statements and #available checks.

@nick.keets hasn’t really said how many platforms he is trying to target besides iOS 13. If any of them do not have CryptoKit—such as Linux—then the #if statements are very much necessary. On the other hand, if...

  • every platform he is targeting contains CryptoKit in its latest version, and
  • the module and all its clients only support development with ≥ Xcode 11

...then you are correct and all the #if statements can be dropped.

When I try to run this on an old iOS 9 device I get: `No such module 'CryptoKit'.

When I run on an iOS 12 simulator I get: dyld: Library not loaded: /System/Library/Frameworks/CryptoKit.framework/CryptoKit

Sorry, for my use case I'm interested only for iOS 9 and later.

Ah, yes. You'll need to tell Xcode that CryptoKit is an optional dependency, since it won't always be available. Go to the "Build Phases" tab of your app target and find the "Link Binary With Libraries" step. Add CryptoKit to the list if it's not already there, and then in the "Status" column, change "Required" to "Optional".

Then you should be good to run on older devices.

I think I'm missing something, because I don't see CryptoKit there.

Normally, what you'd do is hit the plus button at the bottom of the list, search for CryptoKit, and then add it. But as seen here, CryptoKit is not showing up in that list for some reason in Xcode 11.1. So we'll have to do it manually.

Go into the target build settings and search for "Other Linker Flags". You may need to switch the filter toggle in the top left corner from "Basic" to "All". We need to add -weak_framework CryptoKit to the field. (The "weak" part of that flag indicates "Use this framework if available, but do not require it.")

It should look like this:

32%20AM

I'm also going to file an issue with Apple regarding CryptoKit being missing from the list, because it's silly that we should have to do it this way. (FB7416313)

4 Likes

Hello,
I am stuck on this issue.
Adding -weak_framework CryptoKit still doesn't show CryptoKit in Build Phases.
I have been able to run my app on the simulator and and direct on a device using a cable but when I try to archive for uploading I get the 'no such module' fail.
I added CryptoKit for Apple login. My app has been in production for 2 years and its min deployment is iOS 10.
I need to get this update uploaded for test.
If you can give any further help it would be greatly appreciated.
I tried the script mentioned in xcode11 - Getting 'no such module' error when importing a Swift Package Manager dependency - Stack Overflow.

UPDATE.
Updating the target of my app to iOS 11.4 from 10.0 fixed the problem.

1 Like

Same problem, ios11.4 solved it, thank you!