KeyringAccess: A simple, pure Swift way to store sensitive data in Linux Apps

Repository Link

TLDR

If you needed a simple way to securely store credentials from your app on desktop linux, without system dependencies, here you go.

Why

The Swift ecosystem for apps on Apple devices is quite established. For a lot of the tedious work, there are already packages with a nicer, simplified API.

This is not so much the case for Linux, especially not the Swift app ecosystem.

If you ever needed to store sensitive data, like an API key, passwords,… there’s a good chance you have used KeychainAccess before.

While adding DB level encryption to my (pre-release) Framework Amethyst Vein, I ran into the problem, that I had no way to store the key securely.

Missing things like that might keep potential adaptors of SwiftCrossUI away. Storing secrets is basic infrastructure. As a contributor of SwiftCrossUI I can’t have that.

How

On Linux desktops, there is a standart called Secret Service by freedesktop. Its exposed by for example Gnome Keyring, KDE Wallet or KeePassXC over a dbus socket.

One way to work with Secret Service would be wrapping libsecret. That would mean c interop and requiring libsecret development headers being installed. Not a great DX in my opinion.

The alternative is to directly interface with it over DBus. Luckily, there is wendylabsinc/dbus by (among others) @Joannis_Orlandos with a DBus implementation using SwiftNIO.

Process

After testing the basics (establishing a session) to get a basic feel for how stuff works, I decided I wanted the transfer of secrets to be encrypted. The default to do so is dh-ietf1024-sha256-aes128-cbc-pkcs7. Diffie Hellmann using the 1024 bit prime from the ‘Second Oakley Group’ described in RFC 2409, SHA256 hasing the result (= shared secret) and using AES128 in CBC (Cipher Block Chaining) mode with PKCS7 to encrypt/decrypt the secrets.

Unfortunately I didn’t quickly find an existing Swift implementation of that, so I did it myself. I used CNIOBoringSSL for the BigInt math, after I noticed the BigInt library to be relatively slow with powmod. CNIOBORINGSSL also exposes a constant time version of powmod, so a win over all.

Now having a secure connection I just implemented the spec piece by piece, adding integration tests along the way as well as running them in CI.

If you need to do complex stuff with the Secret Service API or want to use collections other than default, use the exposed SecretService library.

This is not the case for most apps though, so I implemented a simplified API, inspired by KeychainAccess. You can use it via import KeyringAccess.

It handles incoming prompts (to unlock something or verify an action) automatically. Also if no default collection exists, it checks whether there is a login collection. If there is, it gets promoted to default, otherwise a new collection is created and made default.

How to use it?

There are async and sync options for every operation. Use of asynchronous API is recommended. The sync API is unavailable in async contexts to avoid deadlocks. Using the sync API for occasional reads/writes should be no issue, but if you need to process larger amounts of data, please consider using the asynchronous batch API, to avoid unnecessary DH and blocking the thread the whole time.

On startup you need to set a global app identifier to avoid accidental conflicts with other apps using this library.

Keyring.appIdentifier.withLock { identifier in
    identifier = "com.example.YourApp"
}

Create the Keyring for a domain or service:

var keyring = Keyring(server: "https://api.yourapp.com/")
    .label("YourApp API key")

Setting a label is optional, but recommended, so it doesn’t show up as blank in the system credential manager. The label is applied to every item. you can use a new label with the same server:

var newKeyring = keyring.label("Some other label")

Store secrets:

keyring["user"] = “mytoken123"
keyring.set("mytoken123", for: "user")

Retrieve secrets:

let secretViaSubscript = keyring["user"]
let secretViaGetter = keyring.get(for: "user”)

Delete by setting to nil:

keyring["user"] = nil
keyring.set(nil, for: "user”)

Get attributes:

if let attributesViaSubscript = keyring[attributes: "user"] {
    print("Label: \(attributesViaSubscript.label)")
    print("Created at: \(attributesViaSubscript.created)")
    print("Modified at: \(attributesViaSubscript.modified)")
}

if let attributesViaGetter = keyring.getAttributes("user") {
    //...
}

Notes

I hope this will be helpful for adaptors of Swift for apps on Linux. If you encounter any problems, please let me know by creating an issue on the package’s GitHub page. Questions are welcome too, here or in issues.

14 Likes