jecrooks.com


Using the iOS Keychain from RoboVM in Scala

Feb 15, 2016

At work we use RoboVM to cross-compile Scala onto iOS for our iPhone app. In theory this is great because we can keep the codebase in just Scala, limiting the required language knowledge to just Scala and preventing context-switching costs. In practice, I'm skeptical of the benefits. Maybe it's our collective lack of knowledge of Scala, I picked up the language explicitly for this job, but Java inevitable leaks into the project regardless, so there goes the 'only one language' part. I don't think this is a necessary consequence of using a JVM language; certainly JVM details leak out much like machine architecture leaks into non-trivial C programs, but Clojure does a pretty good job of preventing major Java leakage. Similarly, I'm wary of Scala-to-JavaScript transpiling for the front-end, in part because calling into APIs in the target language inevitable involves a lot of boilerplate wrapping and/or details of the calling language leak into the code. For me, at least, this breaks the context-switching promise. In fact, it's worse, as I'm now keeping two language models in my head at the same time.

I encountered this problem the other day working with the iOS keychain. Like I said, we're using RoboVM to cross-compile to iOS. RoboVM is actually a Java library, which works well with Scala, but nonetheless we're now dealing with three languages: Scala, Java, and Objective-C. The point of RoboVM is that you don't have to know anything about Obj-C, but unfortunately (at this time) much of RoboVM is essentially a thin wrapper on the Obj-C iOS API. Many modules, such as the security module where the Keychain access lives, are undocumented aside from Javadoc autogenerated documentation. This means that rather than having a semantically meaningful API for Keychain or other security actions, you end up just writing Obj-C code in Java. I spent a long time trying to find a solution to this; as best as I can tell there aren't any examples of using Keychain from RoboVM, let alone in Scala, so here's one. Plus, the version of this that lives in our code base is tagged with 'black magic', so I'll benefit from the documentation next time I have to look at it. For those who just want the final code now, go straight to the end.

Keychain Basics

Now, there are several good guides on using the iOS keychain in Objective-C or Swift. A good default choice is, of course, Apple's documentation. Even better, in my opinion, is Andy Ibanez' tutorial. If you've never used the Keychain before, these are good for the fundamentals. This article will focus on simple storing and retrieving of data from the keychain, using it as a secure key value store where the values (but the not the keys) are stored securely. This is useful for storing, e.g. access tokens for OAuth services, log-in information, etc.

If you read the links above or look up keychain use examples on Stack Overflow, you'll find that items are added by the SecItemAdd() function, which takes a key-value dictionary that include the key and value for the data, as well as requiring that several specific keys be set that control the Keychain behavior. We're going to save a GenericPassword to the keychain, which associates an account and service with a password. Specifically, this associates a pair of strings (account and service) with an opaque data value, so despite the names, this is essentially a key-value system. To save a GenericPassword to the keychain, we really only have to set those three fields in an NSMutableDictionary and set kSecClass to GenericPassword. Not so hard, really. In Objective-C, we could do this via (modified from Ibanez' example)


  NSMutableDictionary *keychainItem = [NSMutableDictionary dictionary];
        
  NSString *service = @"jecrooks.com"
  NSString *account = @"emptylist"
  NSData *password = [@"password" dataUsingEncoding:NSUTF8StringEncoding];

  //Set the dictionary fields
  keychainItem[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
  keychainItem[(__bridge id)kSecAttrService = service
  keychianItem[(__bridge id)kSecAttrAccount = account
  keychainItem[(__bridge id)kSecData = password

  SecItemAdd(keychainItem)
        

Note: I'm not an Objective-C programmer; at the time of writing this, my knowledge of Objective-C consists entirely of reading the Keychain examples and translating them to Java/Scala, so I make no guarentees the above can even compile, let alone do the right thing. Also, I've completely ignored error handling in the above, I'll address this in the Scala version.

Back to Scala and RoboVM

With that aside out of the way, let's translate this to RoboVM calls. Looking at the RoboVM documentation, we find a SecItem class that has an add method taking a SecAttributes argument. Now, note that SecAttributes inherits from CFDictionaryWrapper, it will take the place of the NSMutableDictionary used in the Objective-C version. SecAttributes has, in true Java style, getters and setters for the constant fields like kSecClass. Except that there's no setter for kSecClass specifically. Curiously, it's implemented for the SecQuery class, but not for the SecAttributes class, which is the only class we can pass to SecItem.add(). What fun! In any case, this caused quite the problem until my colleague got the bright idea to, you know, look at the source code for RoboVM, which at least used to be open source up until version 1.8. Since SecAttributes is just a wrapper for a mutable dictionary with a bunch of methods for setting specifically named fields, there's nothing that actually stops us from setting the corresponding field for kSecClass field directly, and looking at the source, we find that all these methods do is specify a key for the underlying dictionary anyway. To make sure we grab the right field, we'll borrow from the SecQuery class's keys, so in Scala (or similarly in Java), we can do this as


  val keychainItem = new SecAttributes()
  item.set(SecQuery.Keys.Class(), SecClass.GenericPassword.value)
        

We have to do a similar dance to set the data field, which we can pull from SecValue. The remaining fields we need to set exist as methods on the SecAttributes class, so we're in the clear there. The remaining issue to deal with is querying to see if the item already exists since adding an item that exists causes iOS to throw an OSStatusException (as does deleting or updating an item that doesn't exist). This leads to that awful pattern of using exceptions for control flow. Okay, so here's the full code for a string-string KV store using Keychain, note that I've also included the type shuffling needed to bridge from Scala-land to NS/CF iOS types:


  package com.jecrooks.blog

  import org.robovm.apple.security.{SecAttributes, SecClass, SecItem, SecQuery, SecReturn, SecValue}
  import org.robovm.apple.foundation.{NSObject, NSData}
  import org.robovm.apple.corefoundation.{CFData, OSStatusException}

  import scala.collection.JavaConversions._
  import scala.util._

  class PasswordStore(val usr: String) extends NSObject {

    def store(k: String, v: String): Unit = {
      val data = new NSData(v.getBytes)
      val keychainItem = new SecAttributes()

      keychainItem.set(SecQuery.Keys.Class(), SecClass.GenericPassword.value)
      keychainItem.setService(k)
      keychainItem.setAccount(usr)
      keychainItem.set(SecValue.Keys.Data(), data)

      val query = new SecQuery()
      query.setClassType(SecClass.GenericPassword)
      query.setAttributes(item)

      Try(SecItem.getMatching(query)) match {
        case Success(_) => Try(SecItem.delete(query)) match {
          case Success(_) => SecItem.add(keychainItem)
          case Failure(ex: OSStatusException) => throw ex
        }
        case Failure(ex: OSStatusException) => ex.getStatus.getStatusCode match {
          case -25300 => SecItem.add(keychainItem)
          case _ => throw ex
        }
      }
    }

    def get(k: String): Option[String] = {
      val attrs = new SecAttributes()
      attrs.setService(k)
      attrs.setAccount(usr)
      attrs.set(SecQuery.Keys.Class(), SecClass.GenericPassword.value)

      val returnOpts = new SecReturn()
      returnOpts.setShouldReturnData(true)

      val query = new SecQuery()
      query.setClassType(SecClass.GenericPassword)
      query.setAttributes(attrs)
      query.setReturn(returnOpts)

      Try(SecItem.getMatching(query)) match {
        case Success(data) => Some(new String(data.asInstanceOf[CFData].getBytes))
        case Failure(_) => None
      }
    }
  }
      
        

Breaking down the code

Imports

The imports are pretty straightforward: the classes from RoboVM we need for accessing Keychain, plus JavaConversions for ergonomics and scala.util._ for Try and company. The Keychain API uses C-style status codes to signal failure, which the RoboVM interface covers with exceptions, so I'm using Scala's Try and pattern matching to use the exceptions for control flow. Notably, I'm bubbling up non-control-flow errors from Try, so it's not really about exception handling here; that's delegated to the caller.

Storing an Item

When adding an item, the code first queries for it. If it exists, we delete it and then add the new version of it. I originally attempted to update it, but I ran into bizarre issues where I always got error codes, so delete and re-add ended up simpler. The Keychain error codes can be found here, but the notable one for us is -25300 which signals 'item not present'. This is the code we get if the item isn't found by the query, as seen in the Failure branch where the code extracts and matches against the status code. In this case, we obviously add the item to Keychain.

Getting an Item

I didn't cover this in Keychain Basics, but to get an item out of Keychain is straightforward, we write a query using the keys and get the data out as binary blob that we have to cast to the appropriate final type. One non-obvious pitfall here is that queries by default don't return the data, just success or failure; we have to tell the query to return the data it finds by passing a SecReturn object with shouldReturnData set to true. Finally, the data is extracted as an opaque blob with non-type CFType. To make Scala's type system happy, we first have to cast to CFData, extract the bytes, and then we can build a string with it.