← KlugKeychain

Documentation

KlugKeychain documentation

com.klug.keychain.Keychain

Keychain storage that survives reinstalls, on iOS and macOS: setup including the macOS keychain group entitlement, guides, the full API and troubleshooting.

iOSmacOS Version 1.1.0 Product page · Buy

Introduction

KlugKeychain stores small string values in the system keychain of iOS and macOS. The keychain is encrypted by the operating system and lives outside your app's container, so its values are still there after the app was deleted and installed again, or after the user deleted the account inside your app.

That makes it the right place for a durable device anchor: for example whether this device has already used a free trial, an installation ID, or a flag that an account was deleted before. The API is four synchronous calls, without events.

Extension IDcom.klug.keychain.Keychain
Current version1.1.0
PlatformsiOS (devices, arm64 and armv7), macOS (Apple silicon and Intel, one universal binary). On every other platform, including Android, Windows and the iOS Simulator, the extension loads, isSupported is false and all calls are safe no-ops.
ActionScript APIOne class: com.klug.keychain.Keychain
Deliverycom.klug.keychain.Keychain.ane (the extension), the ActionScript library (SWC) for code completion, and this documentation

Requirements

AIR SDK51.x (HARMAN)
iOSAny iOS version supported by your AIR SDK (the extension itself runs on iOS 10 and later), real devices
macOSmacOS 11 or later
Signing (iOS)Your normal distribution or development signing; no extra entitlement
Signing (macOS)The app must be signed with a keychain-access-groups entitlement and an embedded provisioning profile for its App ID; otherwise isSupported is false
PermissionsNone. The keychain needs no user permission and shows no prompt.

Add the extension

Self-contained: no core library ANE and no dependency ANEs are needed. One file, one extensionID.

1 · Copy the ANE and declare it

Copy com.klug.keychain.Keychain.ane into your project's extension folder (the folder you pass to ADT with -extdir, or the one your IDE links as native extensions) and add the extension ID to the application descriptor:

app.xml
<extensions>
    <extensionID>com.klug.keychain.Keychain</extensionID>
</extensions>

2 · iOS

Nothing else is needed: no InfoAdditions and no Entitlements. Values are stored in your app's default keychain access group, which iOS derives from your signing.

If your iOS <Entitlements> already declare keychain-access-groups for other reasons, the values go to the first group of that list. Keep that list stable between versions of your app.

3 · macOS entitlement

On macOS the extension uses the modern data-protection keychain, which behaves like the iOS keychain and never shows a password prompt. macOS allows this only for apps signed with a keychain access group. Add these keys to the entitlements you sign the app bundle with (for example codesign --entitlements entitlements.plist). TEAMID and com.example.yourapp are placeholders for your team ID and your app's bundle ID, the <id> of the application descriptor:

entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- keychain group for KlugKeychain -->
    <key>keychain-access-groups</key>
    <array>
        <string>TEAMID.com.example.yourapp</string>
    </array>

    <!-- must match the App ID in the embedded provisioning profile -->
    <key>com.apple.application-identifier</key>
    <string>TEAMID.com.example.yourapp</string>
    <key>com.apple.developer.team-identifier</key>
    <string>TEAMID</string>
</dict>
</plist>

Add these keys to the entitlements your app already uses (for example the App Sandbox keys). Then embed a Mac provisioning profile for this App ID as Contents/embedded.provisionprofile and sign the bundle. A local build signed ad hoc, without a profile, cannot use the keychain group; there isSupported is false and your fallback takes over.

Getting started

All calls are synchronous: they return the result directly, and there are no events.

Main.as
import com.klug.keychain.Keychain;

var keychain:Keychain = Keychain.getInstance();   // singleton: never call new Keychain() yourself

if ( keychain.isSupported )
{
    keychain.set( "installId", "6f1c0b2e" );           // true = stored
    var id:String = keychain.get( "installId" );       // "6f1c0b2e", or "" if nothing is stored
    var known:Boolean = keychain.has( "installId" );  // true
    keychain.remove( "installId" );                    // true, also if it was already gone
}

Guides

Check support once and choose a fallback

isSupported is true on iOS devices. On macOS the extension tests the keychain with a short write and delete and reports true only if that works, which tells you whether the app was signed with the keychain group. Because of that test, read the property once at startup and keep the result. Where the keychain is not available, use your normal app storage; it does not survive a reinstall, but your code keeps working.

DeviceStore.as
import com.klug.keychain.Keychain;
import flash.net.SharedObject;

public class DeviceStore
{
    private var keychain:Keychain = Keychain.getInstance();
    private var useKeychain:Boolean = keychain.isSupported;   // checked once
    private var so:SharedObject = SharedObject.getLocal( "deviceStore" );

    public function write( key:String, value:String ):Boolean
    {
        if ( useKeychain ) return keychain.set( key, value );
        so.data[key] = value;
        so.flush();
        return true;
    }

    public function read( key:String ):String
    {
        if ( useKeychain ) return keychain.get( key );
        return so.data[key] != null ? String( so.data[key] ) : "";
    }
}

Remember a free trial across reinstalls

Store the start of the trial the first time and read it on every start. Deleting and reinstalling the app does not reset it.

Trial.as
private static const TRIAL_DAYS:Number = 7;
private static const KEY:String = "trialStarted";

public function checkTrial():void
{
    var keychain:Keychain = Keychain.getInstance();
    if ( !keychain.isSupported ) { checkTrialOnServer(); return; }   // your app

    if ( !keychain.has( KEY ) )
    {
        keychain.set( KEY, String( new Date().time ) );
        return;                                           // first start: trial begins
    }
    var started:Number = Number( keychain.get( KEY ) );
    var days:Number = ( new Date().time - started ) / 86400000;
    if ( days > TRIAL_DAYS ) showPurchaseScreen();       // your app
}

Remember that an account was deleted on this device

Values in the keychain are independent of your server and of the user's account, so they also outlive an account deletion. Write the flag before you delete the account, and check it before you offer a new trial.

AccountDeletion.as
public function deleteAccount():void
{
    Keychain.getInstance().set( "accountDeletedBefore", "true" );
    deleteAccountOnServer();                               // your app
}

public function mayOfferTrial():Boolean
{
    return Keychain.getInstance().get( "accountDeletedBefore" ) != "true";
}

Store structured values

Values are strings. Store numbers with String() and objects as JSON. Keep values small: the keychain is meant for identifiers, flags and tokens, not for files or caches.

Structured.as
var keychain:Keychain = Keychain.getInstance();

keychain.set( "device", JSON.stringify( { id:"6f1c0b2e", firstStart:new Date().time } ) );

var raw:String = keychain.get( "device" );
var device:Object = ( raw != "" ) ? JSON.parse( raw ) : null;

Read, check and remove values

get() returns "" both when nothing is stored and when an empty string was stored. Use has() when you need to tell these apart. remove() returns true when the key no longer exists, also if it was never there. set() replaces an existing value.

SignOut.as
public function signOut():void
{
    var keychain:Keychain = Keychain.getInstance();
    if ( keychain.has( "sessionToken" ) && !keychain.remove( "sessionToken" ) )
        trace( "keychain: token could not be removed" );
    // keep device-level values such as "trialStarted": they belong to the device, not the account
}

Lifetime of the values

App deleted and installed againValues stay. This is the behaviour of the Apple keychain today; Apple does not formally guarantee it, so treat the keychain as durable, not permanent.
App updateValues stay, as long as team ID and bundle ID (and on macOS the keychain group) remain the same.
BackgroundValues can be read whenever the device has been unlocked once since it was started, also while your app runs in the background.
Backups and new devicesValues are not synced through iCloud Keychain. They can move to a new device with an encrypted backup or a device-to-device transfer.
RemovedBy remove(), by erasing the device, or when the device is set up as new.

API reference

com.klug.keychain.Keychain

Extends flash.events.EventDispatcher. Singleton. All methods are synchronous.

getInstance()public static function getInstance():Keychain
Returns the single instance and creates it on the first call. Calling new Keychain() a second time throws an Error.
isSupportedpublic function get isSupported():Boolean
iOS devices: true. macOS: true if a test write to the keychain succeeds, which requires the keychain group entitlement. Other platforms: false.
set()public function set(key:String, value:String):Boolean
Stores value under key, replacing an existing value. Returns true on success; false if unsupported, if key or value is null, if key is empty, or if the keychain refused the write.
get()public function get(key:String):String
The stored value, or "" if none is stored, the key is null or empty, or the platform is unsupported.
has()public function has(key:String):Boolean
True if a value is stored for key. False if unsupported or the key is null or empty.
remove()public function remove(key:String):Boolean
Deletes the value. Returns true if the key no longer exists afterwards, also if it was already absent; false if unsupported, the key is null or empty, or the keychain refused the deletion.

Events

KlugKeychain dispatches no events. Every method returns its result directly, as described in the API reference.

Troubleshooting

isSupported is false on macOS

The app is not signed with a keychain-access-groups entitlement and a matching provisioning profile, for example in a local build signed ad hoc. Check the entitlements of the signed app with codesign -d --entitlements - YourApp.app, make sure the group starts with your team ID, and that the embedded profile belongs to the same App ID.

isSupported is false on iOS

The native part is built for iOS devices. In the iOS Simulator the fallback is used. Test on a device.

get() returns "" although I stored a value

Check the return value of set(): false means the value was not stored. Keys are case-sensitive. If you changed the team ID, the bundle ID or the keychain group of your app, values written by the old build are not visible to the new one.

The value is gone after I reset the device or set up a new one

Erasing a device removes its keychain. A new device receives the values only through an encrypted backup or a device-to-device transfer.

Reading isSupported is slow on macOS

On macOS every read of the property performs a small keychain test. Read it once and keep the result, as in the guide.

What should I use on Android?

Android has no keychain that survives uninstalling an app. For a reinstall-proof anchor on Android, use the device ID from KlugDeviceId; on Android isSupported of KlugKeychain is false and every call is a safe no-op.

Can I store passwords?

Technically yes, the keychain is encrypted. Remember, though, that the values outlive uninstalling the app; remove credentials with remove() when the user signs out.

Changelog

1.1.0

  • macOS support: the same API on the Mac, using the data-protection keychain, as a universal binary for Apple silicon and Intel
  • macOS: isSupported reports whether the keychain really works, so the app can fall back when the entitlement is missing

1.0.0

  • First release for iOS: set, get, has and remove on the system keychain; values survive uninstalling and reinstalling the app