Categories
Podcast Swift

AppForce1: Episode 3

Welcome to my 3rd episode.

The focus of this episode will be a bit more on basic Swift related items. I will also mention a great online event next month you should check out. CocoaHeadsNL has released the video from their October meetup with Donny Wals. I have some great news about upcoming interviews I will be releasing in the coming weeks.

Please rate me on Apple Podcasts.
Sponsor me on pod.fan/appforce1
Send me feedback on SpeakPipe
Or contact me through twitter.

00:00 Intro

01:01 Welcome

01:13 @AppForce1

01:16 SpeakPipe

01:23 Results 1st interview

01:59 Monday morning tweet

03:45 Mobile Optimized 2020

03:56 CocoaHeadsNL: Video Donny Wals

04:07 CocoaHeadsNL: RSVP

04:14 Under the Radar: Marco Arment on Overcast and SwiftUI

04:36 Obscure APIs

04:59 Progress Expectations

05:33 Porting iPhone app to iPad

06:02 Presenting Popovers from SwiftUI

06:48 How to Create App Clips

07:13 JSON Parsing in Swift

07:41 Formatting numbers in Swift

08:16 Getting the number of days between two dates in Swift

08:45 Swift Concurrency Roadmap

Categories
Random thoughts

AppForce1: Special 1

Antoine is an iOS developer with a good following on Twitter. He often helps CocoaHeadsNL when he can. He works for WeTransfer, writes on his blog and recently launched version 3 of RocketSim.

Categories
Podcast Swift

AppForce1: Episode 2

Welcome to my 2nd episode.

The focus of this episode will be a bit more on SwiftUI related items. I will also mention a great online event next month you should check out. CocoaHeadsNL has announced their november and december planning.
Please rate me on Apple Podcasts.
Sponsor me on pod.fan/appforce1
Send me feedback on SpeakPipe
Or contact me through twitter.

00:00 Intro

00:46 Special: Antoine van der Lee

00:58 Feedback: Speakpipe

01:07 Home Screen Quick Actions — SwiftUI 2.0

01:27 Getting started with the Combine framework in Swift

01:49 The magic of redacted modifier in SwiftUI

02:15 Updating your apps with silent push notifications

02:32 Connecting and merging Combine publishers in Swift

02:56 Evolution of the programming languages from iPhone OS 1.0 to iOS 14

03:12 How Swift API Availability Works Internally

03:48 Nova is here

04:14 How to pass data between views using Coordinator pattern in Swift

04:37 Donny’s monday morning

06:08 CocoaHeadsNL meetups

06:16 Sponsor CocoaHeadsNL

06:20 SwiftAlps

06:42 Outro

06:52 Sponsor me

06:56 Feedback?

07:05 Remember the special

Categories
Podcast Random thoughts

Podcasting gear

That didn’t take very long. After publishing my first episode I got a question almost right away on what I used to record my show.

Here’s the list:

I took a while picking and selecting what to get. I initially wanted a Audio-Technica ATR2100x-USB but it has been unavailable everywhere for a couple of weeks now. So I just bit the bullet and went for a higher spec setup. I specifically went for a cardioid dynamic microphone and not a condenser. I wanted more of that broadcast sound and at the same time avoid picking up unwanted background noise as much as possible. A condenser, or so I read, picks up way more background noise.

Marco Arment’s microphone blog post was very helpful in getting to know some basics and find a good microphone amp suggestion.

For hosting I use Buzzsprout.

All product links are affiliate links.

Categories
Podcast Random thoughts

AppForce1: First episode is live

Categories
Podcast Random thoughts

AppForce1 podcast feed availability

My new podcast is now listed on Apple Podcasts as well. This means you can now find my podcast in your favourite podcast player app by searching for “AppForce1” and get my first episode as soon as it is available next week.

Categories
Podcast Random thoughts

AppForce1 podcast

I’m working on launching my new podcast. Working title “AppeForce1 podcast”.
Just playing with editing and sound bites a little bit. Here’s an initial promo.

Categories
Random thoughts Swift

Tuist.io

At the Dutch CocoaHeads meetup in september I presented about Tuist.

Our path to micro frameworks using Tuis.io

In this video I show how we at Achmea changed the architecture of our app to use µ frameworks with Tuist.io. I demonstrates how to create a sample app and shows the new structure of the ‘Even Appen’ app.

Categories
Random thoughts Swift

Github Actions and MacOS based Actions charged at 10x

Something I forgot to mention on my previous post on GitHub Actions.

GitHub charges 10 build minutes for each MacOS build minute. Be aware of this when working with GitHub Actions for your Mac based builds. It is probably worth it to try and off load as much work as possible to a Linux based image.

SwiftLint and other code analysis is a perfect example of examples of work to be offloaded to a Linux image.

Categories
Security Swift

Store data, encrypted

Today I want to share a helper class I worked on a while back. The requirement was that some (large) piece of data needed to be stored on the device, it should be stored encrypted. File protection complete was deemed not enough.

The idea was that a piece of data needed to be stored on disk, with a key stored in the keychain, in the secure enclave if available.

Just a few lines, but there’s actually a lot going on. The code created was inspired by various bits and pieces online and it took a bit of trial and error to get right.

First things first. We need to determine if a Secure Enclave is available and functional.

import Foundation
import os.log

enum CryptoError: Error {
    case keyCreationFailed
}
class Crypto {
    
    static var shared: Crypto {
        return self.sharedInstance
    }
    
    private static let sharedInstance = Crypto()
    
    private let keyName = "disk_storage_key"
    
    private let hasSecurityEnclave: Bool
    private let algorithm: SecKeyAlgorithm
    private let keySize: Int
    private let keyType: CFString
    
    init() {
        var attributes = [String: Any]()
        attributes[kSecAttrKeyType as String] = kSecAttrKeyTypeEC
        attributes[kSecAttrKeySizeInBits as String] = 256
        attributes[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave
        
        var error: Unmanaged?
        let randomKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)
        
        if randomKey != nil {
            self.hasSecurityEnclave = true
            self.keySize = 256
            self.keyType = kSecAttrKeyTypeEC
            self.algorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
        } else {
            self.hasSecurityEnclave = false
            self.keySize = 4096
            self.keyType = kSecAttrKeyTypeRSA
            self.algorithm = .rsaEncryptionOAEPSHA512AESGCM
        }
    }
}

That actually quite a lot of code already isn’t it. But if you follow along in the above code, it’s not that complicated. We just try to create a random key that requires the Secure Enclave. To my knowledge this generates a key pair, but does not store anything in the keychain or the Secure Enclave. Based on the result, a couple properties are stored on the Crypto class.

Generating a new key pair

Next up is generating a key pair and actually hanging on to it.

private func makeAndStoreKey(name: String) throws -> SecKey {
        
        guard let access =
            SecAccessControlCreateWithFlags(
                kCFAllocatorDefault,
                kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                self.hasSecurityEnclave ? SecAccessControlCreateFlags.privateKeyUsage : [],
                nil) else {
                    throw CryptoError.keyCreationFailed
        }
        var attributes = [String: Any]()
        attributes[kSecAttrKeyType as String] = self.keyType
        attributes[kSecAttrKeySizeInBits as String] = self.keySize
        #if targetEnvironment(simulator)
        print("SIMULATOR does not support secure enclave.")
        #else
        if self.hasSecurityEnclave {
            attributes[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave
        }
        #endif

        let tag = name
        attributes[kSecPrivateKeyAttrs as String] = [
            kSecAttrIsPermanent as String: true,
            kSecAttrApplicationTag as String: tag,
            kSecAttrAccessControl as String: access
        ]

        var error: Unmanaged?
        let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)

        if let error = error {
            throw error.takeRetainedValue() as Error
        }

        guard let unwrappedPrivateKey = privateKey else {
            throw CryptoError.keyCreationFailed
        }

        return unwrappedPrivateKey
    }

Why is using these keychain functions so verbose? A lot of stuff going on. But if you read through it carefully you will see that, again, a key pair is generated, but now we actually hang on to it. See that kSecAttrIsPermanent being give a value of true?

Also notice that there is some code to deal with a simulator not having a Secure Enclave.

Loading a key

We did most of the hard work already now. We also need some code to load a key. We’ll get to choosing between loading and generating in a bit. Key take away in the next bit is that the resulting SecKey is optional. We just might try and load a key and if it fails, we create one…

    private func loadKey(name: String) -> SecKey? {
        let tag = name
        let query: [String: Any] = [
            kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: tag,
            kSecAttrKeyType as String: self.keyType,
            kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
            kSecReturnRef as String: true
        ]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        guard status == errSecSuccess else {
            return nil
        }
        return (item as! SecKey) // swiftlint:disable:this force_cast
    }

Let’s encrypt

Now we are getting to the good part. Let’s encrypt (pun intended).

func encrypt(data clearTextData: Data) throws -> Data? {
        let key = try loadKey(name: keyName) ?? makeAndStoreKey(name: keyName)

        guard let publicKey = SecKeyCopyPublicKey(key) else {
            // Can't get public key
            return nil
        }
        let algorithm: SecKeyAlgorithm = self.algorithm
        guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else {
            os_log("Can't encrypt. Algorithm not supported.", log: Log.crypto, type: .error)
            return nil
        }
        var error: Unmanaged?
        let cipherTextData = SecKeyCreateEncryptedData(publicKey, algorithm,
                                                       clearTextData as CFData,
                                                       &error) as Data?
        if let error = error {
            os_log("Can't encrypt. %{public}@", log: Log.crypto, type: .error, (error.takeRetainedValue() as Error).localizedDescription)
            return nil
        }
        guard cipherTextData != nil else {
            os_log("Can't encrypt. No resulting cipherTextData", log: Log.crypto, type: .error)
            return nil
        }

        os_log("Encrypted data.", log: Log.crypto, type: .info)
        return cipherTextData
    }

Did you notice the optional chaining on the loadKey call?

Let’s decrypt

Having encrypted data is fun and all, but we do need some way to decrypt it again. Notice that in the next bit, being unable to load a key is a failure. When decrypting we do expect that a key is already present.

func decrypt(data cipherTextData: Data) -> Data? {
        guard let key = loadKey(name: keyName) else { return nil }

        let algorithm: SecKeyAlgorithm = self.algorithm
        guard SecKeyIsAlgorithmSupported(key, .decrypt, algorithm) else {
            os_log("Can't decrypt. Algorithm not supported.", log: Log.crypto, type: .error)
            return nil
        }

        var error: Unmanaged?
        let clearTextData = SecKeyCreateDecryptedData(key,
                                                      algorithm,
                                                      cipherTextData as CFData,
                                                      &error) as Data?
        if let error = error {
            os_log("Can't decrypt. %{public}@", log: Log.crypto, type: .error, (error.takeRetainedValue() as Error).localizedDescription)
            return nil
        }
        guard clearTextData != nil else {
            os_log("Can't decrypt. No resulting cleartextData.", log: Log.crypto, type: .error)
            return nil
        }
        os_log("Decrypted data.", log: Log.crypto, type: .info)
        return clearTextData
    }

Conclusions

A couple things to notice in all of the above.