---
title: 'The Keychain'
source: 'https://academia.sh/en/courses/data-networking-and-concurrency/the-keychain'
course: 'Data, Networking and Concurrency'
language: en
updated: '2026-10-21T05:03:09+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates secret values, which belong to the keychain, from the preference store'
    - 'Measures that adding the same item a second time changes the status code'
    - 'Verifies that an update-or-add operation is order-sensitive with two different values'
    - 'Shows that including or excluding the status code in the compared state changes the class'
---

# The Keychain

A measurement of how adding the same item a second time changes the status code where secret values are stored, and how an update-or-add operation is order-sensitive.

In the previous lesson we saw that the preference store keeps its values in the open. For secret values such as a password, a session token, or an application key, the platform has a separate store: the **keychain**. The keychain stores values encrypted, keeps some items even if the app is deleted and reinstalled, and controls access item by item. In return it charges a price: operations return a result code, and that code says whether an item already exists. The question of the course stays the same: **if this operation is repeated or reordered, does the result change?** In this lesson the code becomes part of the result as well.

```swift
// Replay.swift — classifies an operation's sensitivity to repetition and order
struct Trial<State: Equatable & CustomStringConvertible> {
    let label: String
    let once: State
    let twice: State
    let forward: State
    let backward: State

    var sensitivity: String {
        switch (once != twice, forward != backward) {
        case (false, false): return "robust"
        case (true, false): return "repeat-sensitive"
        case (false, true): return "order-sensitive"
        case (true, true): return "doubly-sensitive"
        }
    }

    func report() -> String {
        print("\(label) | once=\(once) twice=\(twice) ab=\(forward) ba=\(backward) -> \(sensitivity)")
        return sensitivity
    }
}

func replay<State>(_ label: String, from start: State, _ a: (State) -> State, then b: (State) -> State) -> Trial<State> {
    Trial(label: label, once: a(start), twice: a(a(start)), forward: b(a(start)), backward: a(b(start)))
}

func countSensitivities(_ results: [String]) {
    for name in ["robust", "repeat-sensitive", "order-sensitive", "doubly-sensitive"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}
```

`once` is the operation applied to the starting state one time, `twice` is two times; `ab` is `a` first and then `b`, and `ba` is the reverse order. The model below is the pure Swift counterpart of the keychain: an item is nothing more than an account name and a value. The model is not the platform itself; it only builds the result the operation leaves in the state.

## Adding and the Status Code

The platform call for adding an item to the keychain reads as follows. The fragment is SOURCE; it needs a real keychain, so it does not run here.

```swift
import Security

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "token",
    kSecValueData as String: Data("a1".utf8)
]

let first = SecItemAdd(query as CFDictionary, nil)   // errSecSuccess
let second = SecItemAdd(query as CFDictionary, nil)  // errSecDuplicateItem
```

The second call comes with the same class and the same account name, so it does not create a new item; it returns `errSecDuplicateItem`. The existing value stays as it is. So the second add has no effect but it has a result, and that result is an error code. Changing an existing item takes a separate call: `SecItemUpdate`. If the item does not exist, that call returns `errSecItemNotFound` as well. Applications combine these two calls as **update-or-add**: first try the update, and add the item if it does not exist.

## The Model of the Keychain

The model keeps a dictionary from account name to value, and the status code of the last operation. The status code enters the comparison; `includesStatus` can turn this off, and the bounding measurement makes use of that.

```swift
// KeychainModel.swift — a pure Swift model of the keychain
struct KeychainBox: Equatable, CustomStringConvertible {
    static let success = 0
    static let duplicateItem = -25299
    static let itemNotFound = -25300

    private(set) var items: [String: String]
    private(set) var status: Int
    let includesStatus: Bool

    init(includesStatus: Bool = true, items: [String: String] = [:]) {
        self.includesStatus = includesStatus
        self.items = items
        self.status = KeychainBox.success
    }

    var description: String {
        let pairs = items.keys.sorted().map { "\($0)=\(items[$0] ?? "-")" }
        let text = pairs.isEmpty ? "-" : pairs.joined(separator: ",")
        return includesStatus ? "\(text) status=\(status)" : text
    }

    static func == (lhs: KeychainBox, rhs: KeychainBox) -> Bool {
        lhs.description == rhs.description
    }

    mutating func add(_ value: String, account: String) {
        if items[account] != nil {
            status = KeychainBox.duplicateItem
        } else {
            items[account] = value
            status = KeychainBox.success
        }
    }

    mutating func update(_ value: String, account: String) {
        if items[account] != nil {
            items[account] = value
            status = KeychainBox.success
        } else {
            status = KeychainBox.itemNotFound
        }
    }

    mutating func addOrUpdate(_ value: String, account: String) {
        update(value, account: account)
        if status == KeychainBox.itemNotFound {
            add(value, account: account)
        }
    }
}

func adding(_ value: String, account: String = "token") -> (KeychainBox) -> KeychainBox {
    { box in
        var next = box
        next.add(value, account: account)
        return next
    }
}

func upserting(_ value: String, account: String = "token") -> (KeychainBox) -> KeychainBox {
    { box in
        var next = box
        next.addOrUpdate(value, account: account)
        return next
    }
}
```

The status code numbers are kept the same as the platform's constants: success `0`, duplicate item `-25299`, item not found `-25300`. The comparison and the printing both go through the same `description`; so what shows up in a classification and what counts in an equality do not drift apart.

**PS4.** The model counts only the account name as the identity of an item. On the platform, identity is the combination of attributes such as class, service, and account; modeling it with the account name alone simplifies the measurement.

**PS5.** The model produces status codes from the constants above. The platform's real return values, access control, and device lock were not measured.

## Measuring Three Operations

Three operations are built. The first is adding the same value twice. The second is running the update-or-add operation with two different values. The third is adding two different values directly.

```swift
// main.swift — add and update-or-add
let sameValue = replay("add-same-value", from: KeychainBox(), adding("a1"), then: adding("a1")).report()
let upsert = replay("upsert", from: KeychainBox(), upserting("a1"), then: upserting("b2")).report()
let twoValues = replay("add-two-values", from: KeychainBox(), adding("a1"), then: adding("b2")).report()

countSensitivities([sameValue, upsert, twoValues])
```

```
add-same-value | once=token=a1 status=0 twice=token=a1 status=-25299 ab=token=a1 status=-25299 ba=token=a1 status=-25299 -> repeat-sensitive
upsert | once=token=a1 status=0 twice=token=a1 status=0 ab=token=b2 status=0 ba=token=a1 status=0 -> order-sensitive
add-two-values | once=token=a1 status=0 twice=token=a1 status=-25299 ab=token=a1 status=-25299 ba=token=b2 status=-25299 -> doubly-sensitive
robust: 0
repeat-sensitive: 1
order-sensitive: 1
doubly-sensitive: 1
```

In the first line, the item is `a1` in both states; only the status code differs: `0` and `-25299`. The add had no effect, but the result changed. The order is meaningless, because the two operations are identical. The class is **repeat-sensitive**. This is a different kind of repetition from the counter in the previous lesson: it is not the value but the result the operation reports that changes.

In the second line, when the update-or-add operation is repeated, the result does not change: the item and the code stay the same. Having no effect has solved repetition. But it does not solve order with two different values: `ab` is `b2`, `ba` is `a1`. The last writer wins, and the order decides the winner. The class is **order-sensitive**. The rule sees the inequality; it does not see the cause, that is, that the second call went through the update path.

The third line combines the two effects. The second add fails, so both repetition and order diverge. The item stays with the value of whichever came first, and which came first depends on the order. The class is **doubly sensitive**.

The count is one repeat-sensitive, one order-sensitive, one doubly sensitive; there is no robust.

## Why Update-or-Add

A direct add assumes that the item does not exist beforehand. If the app adds the token at every launch, the call returns an error from the second launch on and the old value stays; the new value is not silently written. This is a common reason for continuing to work with an old token after the token has been renewed. Update-or-add removes this error: repetition no longer produces an error. Its price is the second line above. If two separate sources write different values to the same account, the order decides the winner.

In practice, two decisions are enough. The result of the add is checked: `errSecDuplicateItem` is not a failure but information that the item already exists. If an account can be written by more than one source, which source gets the last word is written down in the design.

## Common Mistakes

- Writing a secret value to the preference store. The keychain exists for this.
- Not checking the result of an add. Even when an error is returned, the app assumes success and continues with the old value.
- Adding directly at every launch. It works on the first launch and fails silently on the later ones.
- Calling the update-or-add path from two separate places. The order decides the last writer and debugging becomes harder.
- Thinking of delete and add as a single step. If the app is interrupted between the two calls, the item is lost.
- Duplicating the account name as a string. A typo opens a new item.

**PS6.** The model runs the calls without interruption and on a single thread. The case where two calls run at the same time was not built.

## The Bounding Measurement

The first limit is whether the status code enters the compared state. The rule sees the inequality; if the status code is not part of the state, the second add is invisible. Next, we run the same three adds with `includesStatus: false`. On the third line, we measure the same pair of operations from a start where the item exists from the beginning.

```swift
// main.swift — without the status code and from a seeded start
let plain = KeychainBox(includesStatus: false)
let withoutStatus = replay("no-status-same-value", from: plain, adding("a1"), then: adding("a1")).report()
let withoutStatusTwo = replay("no-status-two-values", from: plain, adding("a1"), then: adding("b2")).report()
let seeded = replay("status-seeded-start", from: KeychainBox(items: ["token": "a1"]), adding("a1"), then: adding("a1")).report()

countSensitivities([withoutStatus, withoutStatusTwo, seeded])
```

```
no-status-same-value | once=token=a1 twice=token=a1 ab=token=a1 ba=token=a1 -> robust
no-status-two-values | once=token=a1 twice=token=a1 ab=token=a1 ba=token=b2 -> order-sensitive
status-seeded-start | once=token=a1 status=-25299 twice=token=a1 status=-25299 ab=token=a1 status=-25299 ba=token=a1 status=-25299 -> robust
robust: 2
repeat-sensitive: 0
order-sensitive: 1
doubly-sensitive: 0
```

When the status code was taken out, the same pair of adds turned **robust**; with two different values, the doubly sensitive class dropped to order-sensitive. The operation did not change, only what we look at changed. Taking the code into the state makes repetition visible; leaving it out hides it.

The second limit is the starting state. The same two adds were repeat-sensitive from an empty start. From a start where the item already exists, the first add fails too and the status is pinned at `-25299`: the class is robust. An operation that comes out robust is not robust in every state; it only does not show repetition from this start.

## Where It Shows Up in a Real App

An add operation failing on the second call is one of the most common behaviors of the keychain. An app saves the session key at the first sign-in; when the user signs out and signs in again, the same code runs and the add is rejected because the item already exists. The developer mostly sees this only as an error code, and the user's new key is never stored: the old key stays in place, while the app thinks it has saved the new one. The `-25299` status in the measurement is the counterpart of this collision, and the `add-same-value` line shows that the status changes even when the same value is added a second time.

This distinction matters, because even if the resulting value stays the same, the status code has changed. A caller that reads the code may count the second add as a failure and show an error screen. In the bounding lines, where we did not take the status code into account, the same pair of operations looks robust. The difference is what was included in the equality in the measurement; the rule sees the inequality, but we choose what counts as equal.

Besides sign-out and sign-in, keychain calls are also repeated when the app reopens, when restoring from a backup, and when two separate extensions share the same item. Two places writing a shared item is the real counterpart of the two-value add line in the measurement: which value remains depends on which one ran first.

## A Wrong Setup and Its Fix

A common wrong setup is deleting first and then adding. The logic goes like this: if the item exists, delete it, then add it; that way there is no collision. This arrangement leaves a gap between the two calls in which the item does not exist at all. If the app is interrupted in this gap, or if another place reads the item at exactly that moment, the user is left without a key. Besides, delete and add are two separate operations; each has its own status code, and both have to be checked.

The fix is the "update, otherwise add" pattern we measured. It tries the update first and adds if the item does not exist. The update is robust against repetition; a second update with the same value changes nothing. The pattern does not solve order, though: if two different values arrive in two orders, the last one written remains. That is why the `upsert` line came out protected against repetition but order-sensitive. What the pattern provides is that the first collision does not turn into an error; it does not answer the question of which value will remain.

A second common mistake is ignoring the status code. Code that does not check the result of the add call takes a failed write for a successful one. The fix is simple and tedious: read the status of every call and proceed only on the expected codes. On an unexpected code, the app should present the user with an understandable result.

## Reading the SOURCE Fragment

The fragment at the start of the lesson shows that an item is defined by a query dictionary. The class, service, and account fields in the dictionary make up the identity of the item; the value is a separate field. The call returns a status code, and the code reports success or which collision occurred. Our model reduced the identity to the account name alone; this is recorded as PS4. In reality, the same account name can be two separate items under two different services, and two writes that collide in the model may never collide in reality.

The access setting in the fragment also stayed outside the model. Decisions such as when an item can be read and whether it is reachable while the device is locked directly affect the status codes. That is why PS5 records the real return values and access control of the status codes as not measured. What is measured in this lesson is the rule itself: which pair of operations falls into which class from which start.

## From Measurement to Design

The three lines of the measurement can be turned into a checklist for any code that writes to the keychain. The first question is what repetition does to the operation. When the add call runs a second time, the status code changes; this decides whether the caller will take the failure for an event. The second question is two different values arriving: if which one remains was not written as a rule, the result is left to chance. The third question is which start the operation ran from. That the status lines come out robust from the seeded start says that, in an app where the item already exists, the add call consistently gives the same error; this consistency is not a guarantee, only an observation.

In practice this means: the code that writes the key in the sign-in flow must not assume whether the item exists before writing. The update-or-add pattern removes this assumption. In the sign-out flow, the code that deletes the item must not give an error when there is no item to delete; making the delete robust against repetition protects a user who presses the sign-out button twice. We did not measure deletion in this lesson, but the same question has to be asked of it, and the answer is found by measuring rather than assuming.

Finally, remember that the value in the keychain is a secret. The outputs of this lesson contain example values; writing a real key to a log or showing it on screen is a separate mistake. The measurement code carries no secret, and your measurement code should not carry one either.

## Summary

- Secret values belong to the keychain; operations return a status code, and that code is part of the result.
- Adding the same item a second time does not change the item but turns the status from `0` to `-25299`; the class is repeat-sensitive.
- Update-or-add solves repetition but does not solve order with two different values: `ab` is `b2`, `ba` is `a1`.
- Adding two different values directly separates both repetition and order; the class is doubly sensitive.
- When the status code is removed from the comparison, the same pair of adds looks robust; it also looks robust if the item exists at the start.
- This lesson's sensitivity: **repeat-sensitive**.

## Next Step

The keychain stores small secret values. In the next lesson, The File System, we move to where larger data is written to files, and we measure that appending to the end of a file is sensitive to both repetition and order.
