Tagged with

Introduction

Using Amazon Web Services, like S3, means using their Signature version 4, a complex take on HMAC. As yet, they have no native-Swift library, but I wanted to go ahead and get started with Swift on the Server, which meant: write it from scratch.

Hash functions from BlueCryptor

The first challenge is finding the appropriate SHA hash functions in Swift. I used IBM's BlueCryptor Swift library, which wraps around the iOS / macOS CommonCrypto library, and on Linux, it wraps OpenSSL.

BlueCryptor technically does accomplish what it sets out to do: wrap underlying C functions in Swift for cross-platform compatiblity, but it does so in very non-Swifty ways. For instance, BlueCryptor wraps several functions which take common beginning, middle, end behaviors, and instead of defining them in terms of protocols, which would allow extensibility, each algorithm becomes a case in an enum.

BlueCryptor provides utilities, like converting anArray of bytes into a hex string of its bytes. But instead of providing this as an extension on an Array of UInt8,

extension Array where Element == UInt8 {

  func asHex(uppercase:Bool=false)->String {...

BlueCryptor serves it as a static function on a struct named CryptoUtils.

struct CryptoUtils {
...
static func hexString(from byteArray: [UInt8], uppercase: Bool = false) -> String

This interface serves no practical function. There is simply a type, CryptoUtils with no instance members. None. Why does it exist? In a language without typed extensions, it might make sense because I need a custom scope for my functions, but not in Swift. When you're designing a Swift interface, think about what makes sense at the call site:

With BlueCryptor:

let hashBytes:[UInt8] = ...
let hashHex:String = CryptoUtils.hexString(from:hashBytes)

properly designed: swift let hashBytes:[UInt8] = ... let hashHex:String = hashBytes.asHex()

Alternatively, one could implement hex conversion as an init method on String`.

extension String {
  init(toHex bytes:[UInt8]) {...

But again, consider the call site when our array of bytes is an optional.

The simplest call as an init extension on String: swift let hashBytes:[UInt8]? = ... let hashHex:String? = hashBytes.flatMap{String(toHex:$0)}

properly designed: swift let hashBytes:[UInt8]? = ... let hashHex:String? = hashBytes?.asHex()

Declaring this particular behavior as a computed extension on an [UInt8] makes the call site the shortest, and loses no behavior.

Avoiding Name Collisions in Swift extensions

While Swift is perfectly capable of correctly handling two extensions with identical names in separate packages, it disambiguates those by knowing which packages have been imported into the file which contains the call-site. If, as is often the case, you use two similar extensions in the same file, you'll need a way to disambiguate them.

As a legacy from Obj-C, in which the linker couldn't handle two extensions with the same name from two different dependencies, many developers use an underscore followed by an abbreviation for their package name:

extension Array where Element == UInt8 {

  func asHex_bc(uppercase:Bool=false)->String {...

While there are ways around the naming collision in Swift, using this same technique means your user won't have to find them. As an aside, other developers prepended their abbreviation with an underscore. I find this slightly less readable at the call site.

let hashHex:String = hashBytes.asHex_bc()

vs: swift let hashHex:String = hashBytes.bc_asHex()

I find that if I'm reading a function name, and it contains an _, I recognize that as providing an appositive context. But whatever I find right after the . I find as a direct identifier. Neither is right or wrong or changes how anything compiles, it's just that one is easier on the eyes.

Design decisions for SwiftAWSSignatureV4

In designing my cross-platform Swift package for S3, I knew I would end up needing signature version 4 for services other than S3. So instead of baking it into my S3 package, I placed it in its own package.

AWSAccount

Signing for AWS uses a collection of details about a service, an account name and key, the service name and region. I could have (and still could) package these at different levels to represent a program which has the same IAM keys, but for different services. But if I did that, my keys would still need to be securely accessed where they are injected, and I'd still need separate instances of something to wrap each combination of keys and service and region. Since I have yet to actually code such a set-up, I don't know exactly how my code would get used. So... YAGNI. Stick to the case I have.

open class AWSAccount {
  ///such as "s3" or "kms"
  public let serviceName:String
  public let region:String
  public let accessKeyID:String
  ///as a base-64 string
  public let secretAccessKey:String
  public init(serviceName:String, region:String, accessKeyID:String, secretAccessKey:String)

Technically, I could have made this type a struct, and I may go back and revisit that decision. It's a good candidate for a struct, it does nothing besides hold values, and compute non-mutating derivatives of those values.

This collection of values gave me everything I needed to create sigv4's key for signing.

func keyForSigning(now:DateComponents)->[UInt8]?

URLRequest

Signature v4 relies on knowing all the intimate details of a url request, like a hash of the body data, the url and the headers. And it adds more headers. This led me to two design options:

  1. Write a function on AWSAccount which signs a mutable URLRequest

  2. Write an extension on URLRequest which signs it in place.

If I wrote a function on AWSAccount (and that might be your first impulse if you're coming from JavaScript), I'd end up with inout and &, and that just feels like C.

class AWSAccount {
  func sign(request:inout URLRequest) {...

At the call site, the inout requires an &, and in Swift, inout is really just a work-around.

let account:AWSAccount = ...
var request:URLRequest = URLRequest(url:....)
account.sign(request:&request)

But if I write this behavior as an extension on URLRequest,

extension URLRequest {
  mutating func sign(with account:AWSAccount) {...

then my call site is not quite as messy:

let account:AWSAccount = ...
var request:URLRequest = URLRequest(url:....)
request.sign(with:account)

Another design choice would be to not write this as a mutating func, but as a function which produced a new request, leaving the old one immutable.

extension URLRequest {
  func signed(with account:AWSAccount)->URLRequest? {.

This would allow me to represent failure of the signing (which is technically possible in OpenSSL but not macOS) by returning nil. It also means I wouldn't need to keep track of my instance variable name at the call site:

let account:AWSAccount = ...
guard let request:URLRequest = URLRequest(url:...).signed(with:account) else { ... }

The result is my request is a let instead of a var, which makes screwing it up harder later in the program, and if signing fails, I don't end up with an instance of an unsigned request in my program.

However, URLRequests are more often used as partially-complete value types. For example, we create one with a URL, but headers can't be provided in the init method. Since I have to include all other headers before signing, this means invariably, I already have a request object which is already a var.

var request:URLRequest = URLRequest(url:...)
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")

So the benefits of using the non-mutating design never materialize in real code. They just make the actual call site slightly more complicated:

request = request.signed(with:account)

Conclusion

Writing Swifty code means not only knowing which language features could technically accomplish your goals, but also knowing how the types are used in practice at the call site. Provide as minimal an interface as is necessary to get in, do what needs to be done, and get out.


4c198ffcf0853e6011f58fdcdd32a111?s=184&d=mm

Ben Spratling The day the iPhone app store opened, 3 of its apps were written by Ben, who earned his Ph.D. improving the performance of algorithms for aerospace applications. He’s contributed to lots of apps, from a handful of startups to Wolfram and DOMO. And when he’s not coding, he’s volunteering at his church.