# Keychain Item Classes

The keychain stores five kinds of item: `kSecClassGenericPassword`,
`kSecClassInternetPassword`, `kSecClassKey`, `kSecClassCertificate` and
`kSecClassIdentity`. Choosing the wrong one does not crash anything. It shows up
later as Password AutoFill that never offers the credential, queries that
collide with unrelated items, or keys stored with weaker handling than they
should get.

Each class has its own composite primary key. A second add whose primary-key
attributes equal an existing item's fails with `errSecDuplicateItem` (-25299).
The save pattern (add, then update on duplicate) is in
[keychain-fundamentals.md](keychain-fundamentals.md).

## Contents

- [Primary Keys per Class](#primary-keys-per-class)
- [Generic Password](#generic-password)
- [Internet Password](#internet-password)
- [Key](#key)
- [Certificate](#certificate)
- [Identity](#identity)
- [Mistakes Generated Code Makes](#mistakes-generated-code-makes)
- [Platform Differences](#platform-differences)
- [Size and When to Use a File](#size-and-when-to-use-a-file)
- [Tests That Prove the Class Is Right](#tests-that-prove-the-class-is-right)
- [Moving Web Logins to Internet Passwords](#moving-web-logins-to-internet-passwords)
- [Checklist](#checklist)
- [Related Files](#related-files)

## Primary Keys per Class

For every class, `kSecAttrAccessGroup` and `kSecAttrSynchronizable` are also
part of the key.

| Class | Primary-key attributes | Use it for |
| --- | --- | --- |
| Generic password | `kSecAttrService`, `kSecAttrAccount` | app secrets, API tokens, raw key bytes |
| Internet password | `kSecAttrAccount`, `kSecAttrServer`, `kSecAttrPath`, `kSecAttrPort`, `kSecAttrProtocol`, `kSecAttrAuthenticationType`, `kSecAttrSecurityDomain` | website logins, AutoFill |
| Key | `kSecAttrApplicationTag`, `kSecAttrApplicationLabel`, `kSecAttrKeyType`, `kSecAttrKeyClass`, `kSecAttrEffectiveKeySize`, `kSecAttrKeySizeInBits` | `SecKey` objects |
| Certificate | `kSecAttrIssuer`, `kSecAttrSerialNumber`, `kSecAttrCertificateType` | X.509 certificates |
| Identity | same as certificate | not stored; a join of certificate and private key |

Leaving a primary-key attribute out is legal. The attribute then takes an empty
default, and the next add that also leaves it out collides with the first.

## Generic Password

Pick this class for OAuth refresh tokens, API tokens, symmetric key bytes and
any secret that is not a website login.

- Set `kSecAttrService` (the bundle identifier or a service name) and
  `kSecAttrAccount`. The API will not insist on them, but without them unrelated
  items collide. Put the secret itself, as `Data`, under `kSecValueData`.
- Optional descriptive attributes: `kSecAttrLabel`, `kSecAttrDescription`,
  `kSecAttrComment`, `kSecAttrCreator` and `kSecAttrType` (FourCharCode as
  `CFNumber`), `kSecAttrGeneric` (`CFData`), `kSecAttrIsInvisible`,
  `kSecAttrIsNegative`. The system maintains `kSecAttrCreationDate` and
  `kSecAttrModificationDate`; they are read-only.
- `kSecAttrGeneric` does not count toward uniqueness. Two adds that differ only in
  `kSecAttrGeneric` still collide, yet a query that filters on a different
  `kSecAttrGeneric` value reports the item as missing.

```swift
func addLicenseKey(_ bytes: Data) -> OSStatus {
    var entry: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: "com.example.editor",
        kSecAttrAccount as String: "license",
        kSecValueData as String: bytes,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
    ]
    #if os(macOS)
    entry[kSecUseDataProtectionKeychain as String] = true
    #endif
    return SecItemAdd(entry as CFDictionary, nil)
}
```

The caller switches on the returned status and updates on
`errSecDuplicateItem`.

Decision rule: a credential for a web domain that should appear in Password
AutoFill belongs in an internet password. To the system, a generic password's
service string is an opaque label, so AutoFill never offers it.

## Internet Password

The seven-attribute key is what lets the system connect a credential to a
domain for AutoFill, Safari and syncing across devices.

| Attribute | Meaning |
| --- | --- |
| `kSecAttrServer` | host name |
| `kSecAttrProtocol` | for example `kSecAttrProtocolHTTPS` |
| `kSecAttrPort` | `CFNumber` |
| `kSecAttrPath` | path on the server |
| `kSecAttrAccount` | the user name |
| `kSecAttrSecurityDomain` | the HTTP realm |
| `kSecAttrAuthenticationType` | for example `kSecAttrAuthenticationTypeHTMLForm` |

`kSecAttrService` and `kSecAttrGeneric` do not exist on this class.

AutoFill needs three things together:

1. the credential stored as an internet password with `kSecAttrServer` set;
2. the Associated Domains entitlement containing `webcredentials:<domain>`;
3. the domain's association file, published over HTTPS under the
   `.well-known` directory, with a `webcredentials.apps` array that lists
   `TEAMID.bundle.identifier`.

Safari itself keeps every web password as an internet password, and a web
login stored as a generic password is never suggested.

```swift
// Wrong: the domain is hidden in an opaque service string; AutoFill ignores it.
let hidden: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrService as String: "forum.example.org",
    kSecAttrAccount as String: "rhea",
    kSecValueData as String: Data("pw".utf8)
]
```

```swift
func webLogin(user: String, secret: Data) -> [String: Any] {
    [
        kSecClass as String: kSecClassInternetPassword,
        kSecAttrServer as String: "forum.example.org",
        kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
        kSecAttrPort as String: 443,
        kSecAttrPath as String: "/login",
        kSecAttrAccount as String: user,
        kSecAttrAuthenticationType as String: kSecAttrAuthenticationTypeHTMLForm,
        kSecValueData as String: secret,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked,
        kSecUseDataProtectionKeychain as String: true
    ]
}
```

There are more than thirty protocol constants (`kSecAttrProtocolHTTPS`,
`kSecAttrProtocolHTTP`, `kSecAttrProtocolSSH`, `kSecAttrProtocolFTP`,
`kSecAttrProtocolIMAPS`, `kSecAttrProtocolSMTP`, ...). Authentication types
include `kSecAttrAuthenticationTypeHTMLForm`, `kSecAttrAuthenticationTypeHTTPBasic`,
`kSecAttrAuthenticationTypeHTTPDigest` and `kSecAttrAuthenticationTypeDefault`.

A password manager that wants to supply credentials system-wide builds a
credential provider extension (iOS 12+): subclass
`ASCredentialProviderViewController`, publish `ASPasswordCredentialIdentity`
values to `ASCredentialIdentityStore`, and override
`provideCredentialWithoutUserInteraction(for:)`.

## Key

- `kSecAttrKeyType` and `kSecAttrKeySizeInBits` are required to generate a key.
  `SecKeyCreateRandomKey` without either returns `errSecParam`.
- `kSecAttrKeyClass` (`kSecAttrKeyClassPublic`, `kSecAttrKeyClassPrivate`,
  `kSecAttrKeyClassSymmetric`) is filled in on generation, but queries must name
  it.
- `kSecAttrApplicationTag` (`CFData`) is yours to set, is part of the key, and
  is the attribute to look keys up by. Encode a reverse-DNS name to `Data` for
  it.
- `kSecAttrApplicationLabel` (`CFData`) is computed by the system as the SHA-1
  of the public key (the `subjectPublicKey` bit string from RFC 5280 section
  4.1). It is part of the key, and it has to equal a certificate's
  `kSecAttrPublicKeyHash` for the pair to form a `SecIdentity`. Do not set it
  yourself on asymmetric keys.
- `kSecAttrLabel` (`CFString`) is only a display name. It is not part of the key
  and is what Keychain Access shows on macOS.

Prefer `SecKeyCreateRandomKey` (available from iOS 10 and macOS 10.12) as the way to make a
key: it generates and stores in one step, returns a `SecKey`, computes the
application label, and can target the Secure Enclave with
`kSecAttrTokenID: kSecAttrTokenIDSecureEnclave`. Apple suggests persisting only
the private key and obtaining the public half with `SecKeyCopyPublicKey()`.

```swift
func makeReceiptSigningKey() throws -> SecKey {
    let tag = Data("com.example.receipts.signing".utf8)
    let spec: [String: Any] = [
        kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeySizeInBits as String: 256,
        kSecPrivateKeyAttrs as String: [
            kSecAttrIsPermanent as String: true,
            kSecAttrApplicationTag as String: tag
        ]
    ]
    var failure: Unmanaged<CFError>?
    guard let key = SecKeyCreateRandomKey(spec as CFDictionary, &failure) else {
        throw (failure?.takeRetainedValue() as Error?) ?? KeychainError(status: errSecParam)
    }
    return key
}
```

For a Secure Enclave key, create the access control first and put it inside the
private-key attributes. The flags always include `.privateKeyUsage`:

```swift
func makeEnclaveKey() throws -> SecKey {
    var failure: Unmanaged<CFError>?
    guard let policy = SecAccessControlCreateWithFlags(
        nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        [.privateKeyUsage, .userPresence], &failure) else {
        throw (failure?.takeRetainedValue() as Error?) ?? KeychainError(status: errSecParam)
    }
    let spec: [String: Any] = [
        kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeySizeInBits as String: 256,
        kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
        kSecPrivateKeyAttrs as String: [
            kSecAttrIsPermanent as String: true,
            kSecAttrApplicationTag as String: Data("com.example.enclave.auth".utf8),
            kSecAttrAccessControl as String: policy
        ]
    ]
    guard let key = SecKeyCreateRandomKey(spec as CFDictionary, &failure) else {
        throw (failure?.takeRetainedValue() as Error?) ?? KeychainError(status: errSecParam)
    }
    return key
}
```

`KeychainError` is the status wrapper from
[keychain-fundamentals.md](keychain-fundamentals.md).

Call `SecItemAdd` for a key yourself only when importing one: build a `SecKey`
from external bytes with `SecKeyCreateWithData`, then add it through
`kSecValueRef`. Never add raw key bytes as a key item.

Mapping CryptoKit keys to storage:

| CryptoKit type | How to store |
| --- | --- |
| `P256`, `P384`, `P521` private keys | `SecKeyCreateWithData` with `kSecAttrKeyTypeECSECPrimeRandom`, then a `kSecClassKey` item |
| `Curve25519` keys, `SymmetricKey` | no `SecKey` form; store the raw bytes as a generic password |
| `SecureEnclave.P256` keys | keep the encrypted `dataRepresentation` blob in a generic password item |

Details for the enclave case are in [secure-enclave.md](secure-enclave.md);
the CryptoKit side is in [cryptokit-symmetric.md](cryptokit-symmetric.md) and
[cryptokit-public-key.md](cryptokit-public-key.md).

## Certificate

- Certificates are public, so the keychain does not encrypt them.
- Adding one takes two steps: `SecCertificateCreateWithData(nil, der)` (DER
  bytes only; strip PEM armor first), then `SecItemAdd` with `kSecValueRef`.
- Passing the certificate by `kSecValueRef` lets the keychain extract issuer,
  serial number, subject, `kSecAttrPublicKeyHash`, certificate type and
  encoding for you.
- `kSecAttrPublicKeyHash` has to match a private key's
  `kSecAttrApplicationLabel` for an identity to appear.
- `kSecAttrApplicationTag` is not valid on certificates: the data protection
  keychain returns `errSecParam`, and the file-based keychain fails without
  telling you.

```swift
func storeServerRoot(der: Data) throws {
    guard let cert = SecCertificateCreateWithData(nil, der as CFData) else {
        throw KeychainError(status: errSecParam)
    }
    let entry: [String: Any] = [
        kSecClass as String: kSecClassCertificate,
        kSecValueRef as String: cert,
        kSecAttrLabel as String: "Internal root"
    ]
    let status = SecItemAdd(entry as CFDictionary, nil)
    guard status == errSecSuccess || status == errSecDuplicateItem else {
        throw KeychainError(status: status)
    }
}
```

A duplicate certificate is by definition the same certificate, so there is
nothing to update. Trust evaluation and pinning are in
[certificate-trust.md](certificate-trust.md).

## Identity

There is no identity table. The keychain presents an identity whenever a
certificate's `kSecAttrPublicKeyHash` equals a private key's
`kSecAttrApplicationLabel` (both the SHA-1 of the public key).

- `SecItemAdd` with `kSecClassIdentity` returns `errSecParam` (-50).
- Adding a certificate or a key can make an identity appear; deleting either
  makes it disappear.

Three ways to get one:

1. Import a PKCS#12 file with `SecPKCS12Import`, passing the password under
   `kSecImportExportPassphrase`. Take `kSecImportItemIdentity` from the result,
   and split it with `SecIdentityCopyCertificate` and
   `SecIdentityCopyPrivateKey` when you need the parts.
2. Add the certificate and the private key separately so their hashes match.
   On macOS both must live in the same implementation (both data protection or
   both file-based).
3. On macOS only, `SecIdentityCreateWithCertificate` finds the private key that
   belongs to a certificate.

```swift
func importClientIdentity(p12: Data, password: String) throws -> SecIdentity {
    let options = [kSecImportExportPassphrase as String: password]
    var items: CFArray?
    let status = SecPKCS12Import(p12 as CFData, options as CFDictionary, &items)
    guard status == errSecSuccess,
          let first = (items as? [[String: Any]])?.first,
          let identity = first[kSecImportItemIdentity as String] else {
        throw KeychainError(status: status)
    }
    return identity as! SecIdentity
}
```

(`SecIdentity` is a CF type, so the conditional cast always succeeds once the
value is present; the forced cast is the idiomatic way to bridge it.)

## Mistakes Generated Code Makes

1. A web login saved as a generic password with the domain in the service.
   AutoFill never offers it. Use an internet password with `kSecAttrServer` and
   associated domains.
2. Adding an identity directly. Import PKCS#12, or add certificate and key
   separately.
3. Key queries without `kSecAttrKeyType`. Type is part of the key, so an RSA
   key and an EC key with the same tag are both candidates, and the query can
   return the wrong one.
4. Writing a human-readable string into `kSecAttrApplicationLabel`. The lookup
   attribute is `kSecAttrApplicationTag`.
5. No duplicate handling. Save with add-or-update:

   ```swift
   func upsert(match: [String: Any], payload: [String: Any]) throws {
       let added = SecItemAdd(match.merging(payload) { _, new in new } as CFDictionary, nil)
       switch added {
       case errSecSuccess:
           return
       case errSecDuplicateItem:
           let changed = SecItemUpdate(match as CFDictionary, payload as CFDictionary)
           guard changed == errSecSuccess else { throw KeychainError(status: changed) }
       default:
           throw KeychainError(status: added)
       }
   }
   ```

   `match` holds the class and primary-key attributes; `payload` holds the
   value and the accessibility (accessibility is accepted in an update).
6. macOS code without `kSecUseDataProtectionKeychain`. It lands in the
   file-based keychain, which ignores or mishandles `kSecAttrAccessible`,
   `kSecAttrAccessGroup` and biometric access control.
7. Large blobs. Every read of `kSecValueData` pays for a Secure Enclave
   decrypt. Past a few kilobytes, switch to envelope encryption (below).

## Platform Differences

- iOS, tvOS and watchOS have only the data protection keychain;
  `kSecUseDataProtectionKeychain` is ignored there.
- A native macOS app defaults to the file-based keychain. Mac Catalyst defaults
  to the data protection keychain.
- Apple recommends `kSecUseDataProtectionKeychain: true` on every operation.
  `SecKeychainCreate` is deprecated as of macOS 12.
- TN3137 describes three macOS keychain APIs (the legacy Keychain API,
  `SecKeychain`, and `SecItem`) over two implementations. Only `SecItem` can
  reach either implementation; `SecKeychain` always works on the file-based one.
- On a Mac, reaching the data protection keychain needs a code-signed app
  that carries a provisioning profile.
- The match limit a delete assumes differs by backend: all matching items on
  the data protection keychain, only one on the file-based keychain
  (reported to Apple as bug 105800863). Write delete queries that are precise
  enough for either.

### Synchronization

- Items sync only when `kSecAttrSynchronizable` is set to `true`. Nothing syncs
  by default.
- Every one of the five classes can sync from iOS 14, macOS 11 and watchOS 7; before
  that, only the two password classes.
- A synchronizable item cannot use an accessibility value ending in
  `ThisDeviceOnly`. Apple documents the combination as not allowed, and the add
  fails with `errSecParam` (-50).
- Synchronizable items cannot be fetched through persistent references.
- tvOS accepts the attribute and never syncs.
- Updating or deleting a synced item changes it on every device.

Access groups and iCloud Keychain are covered in
[keychain-sharing.md](keychain-sharing.md).

## Size and When to Use a File

Apple publishes no hard limit and describes the keychain as a place for small
secrets. The SQLite column limit (`SQLITE_MAX_LENGTH`, about 16 MB) is not an
invitation.

- Query with the full composite key, use `kSecMatchLimitOne` when you expect
  one item, and skip `kSecReturnData` when you only need to know an item exists.
- For bulk data, use envelope encryption: keep a data encryption key (AES-256,
  32 bytes) in the keychain, encrypt the payload with it, and write the
  ciphertext to a file that also has `NSFileProtection` set. OWASP MASTG
  recommends this layout for MASVS L2.
- File protection counterparts: `kSecAttrAccessibleWhenUnlocked` matches
  `NSFileProtectionComplete`; `kSecAttrAccessibleAfterFirstUnlock` matches
  `NSFileProtectionCompleteUntilFirstUserAuthentication`.

Requesting data and attributes together returns a dictionary:

```swift
func describeLicense() throws -> [String: Any]? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: "com.example.editor",
        kSecAttrAccount as String: "license",
        kSecReturnAttributes as String: true,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]
    var out: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &out)
    if status == errSecItemNotFound { return nil }
    guard status == errSecSuccess else { throw KeychainError(status: status) }
    return out as? [String: Any]
}
```

The dictionary includes the account (`acct`), service (`svce`), creation and
modification dates, accessibility, the synchronizable flag, and the secret
under `kSecValueData`. Read them with the `kSec...` constants, not the short
strings. Switch to `kSecMatchLimitAll` and you get back a list of them.

## Tests That Prove the Class Is Right

| Action | Expected status |
| --- | --- |
| Add with `kSecClassIdentity` | `errSecParam` |
| Add a key without `kSecAttrKeyType` | `errSecParam` |
| Add twice with the same primary key | `errSecDuplicateItem` |
| Add with synchronizable `true` and a `ThisDeviceOnly` value | `errSecParam` |
| Add a certificate with `kSecAttrApplicationTag` | `errSecParam` |
| Query a key without `kSecAttrKeyClass` | may match the wrong class |

The harness for these tests is in
[testing-security-code.md](testing-security-code.md).

## Moving Web Logins to Internet Passwords

```swift
func promoteWebLogins(oldService: String, host: String) throws {
    let find: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: oldService,
        kSecReturnAttributes as String: true,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitAll
    ]
    var out: CFTypeRef?
    let status = SecItemCopyMatching(find as CFDictionary, &out)
    if status == errSecItemNotFound { return }
    guard status == errSecSuccess, let rows = out as? [[String: Any]] else {
        throw KeychainError(status: status)
    }
    for row in rows {
        guard let user = row[kSecAttrAccount as String] as? String,
              let secret = row[kSecValueData as String] as? Data else { continue }
        let match: [String: Any] = [
            kSecClass as String: kSecClassInternetPassword,
            kSecAttrServer as String: host,
            kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
            kSecAttrAccount as String: user,
            kSecUseDataProtectionKeychain as String: true
        ]
        try upsert(match: match, payload: [
            kSecValueData as String: secret,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ])
        let removeOld: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: oldService,
            kSecAttrAccount as String: user
        ]
        let removed = SecItemDelete(removeOld as CFDictionary)
        guard removed == errSecSuccess || removed == errSecItemNotFound else {
            throw KeychainError(status: removed)
        }
    }
}
```

The old item is removed only after the new one was written. Broader migration
guidance is in [migration-legacy-stores.md](migration-legacy-stores.md).

## Checklist

- Web-domain credentials are internet passwords.
- Every primary-key attribute for the class is set.
- Nothing depends on `kSecAttrGeneric` for uniqueness.
- Keys are found by `kSecAttrApplicationTag`; `kSecAttrApplicationLabel` is left
  to the system.
- Identities come from `SecPKCS12Import` or a matching certificate and key.
- Key items always state type and size.
- macOS code sets `kSecUseDataProtectionKeychain: true`.
- Synchronizable items never use a `ThisDeviceOnly` accessibility value.
- Only small secrets go in the keychain; larger data uses envelope encryption.
- Certificates carry no application tag and get their metadata from
  `kSecValueRef`.
- `errSecDuplicateItem` leads to `SecItemUpdate` (add, then update).

## Related Files

[keychain-fundamentals.md](keychain-fundamentals.md),
[keychain-access-control.md](keychain-access-control.md),
[biometric-authentication.md](biometric-authentication.md),
[credential-storage-patterns.md](credential-storage-patterns.md),
[common-anti-patterns.md](common-anti-patterns.md),
[compliance-owasp-mapping.md](compliance-owasp-mapping.md).
