ykrods note

macOS でのキーチェーンの挙動

Security.framework でキーチェーンを操作する方法を調べていたが、仕様とか概念レベルのことからして公式のドキュメントを読んでもよくわからなかったので、コードを書いて挙動を確認することにした。

文章としてはとりあえず動かしたりキーチェーンアクセスをいじったり色々読んだ結果を先にまとめて、後ろに検証で使ったコードをおく形をとる。

注釈

iOS では単一のキーチェーンしか対応していないため 1 、この文章の内容はあまり関係ない

まとめ

キーチェーン

  • macOS ではキーチェーンを複数持つことができる。例えばキーチェーンアクセスを開くと左上のリストに「ログイン」と「システム」があるが、これらは別々のキーチェーン。

  • キーチェーンには「パスワード」「証明書(+秘密鍵)」「秘密メモ」などの要素を格納できる

キーチェーンのロック

  • キーチェーンはロック・アンロックの状態を持つ。キーチェーンに設定されたパスワードを入力してアンロックを行う。

    • キーチェーンアクセスで南京錠のマークが開いたり閉じたりするが、これはロック状態を表している

  • キーチェーンのロック状態はコネクション(セッション?)単位でなく、システム単位で持つ

    • つまり、あるキーチェーンをあるアプリでアンロックした状態で別のアプリでキーチェーンを開いた場合、既にアンロック状態になっているということ

キーチェーンの設定

キーチェーン単位で以下の項目が設定できる 2

  • 一定時間操作がなかった場合に自動的にロック(デフォルトでは5分間)

  • PCがスリープ状態になった場合に自動的にロック

  • パスワードの再設定

キーチェーンの実装

  • キーチェーンは内部的には SQLite ベースの db として実装されている 3 。ファイルは ~/Library/Keychains/ 以下に配置される。

  • キーチェーンは単なるファイルだが、キーチェーンのAPI (Security.framework) は直接ファイルを開くような実装にはなっておらず、securityd というデーモンに対してやりとりする形になっている 4

    • ロック状態は多分(おそらく) securityd が持っている

キーチェーンの要素

  • キーチェーンには「パスワード」「証明書(+秘密鍵)」「秘密メモ」などの要素(データベース的に言えば行)を格納できる

  • キーチェーン要素はパスワードや秘密鍵といった秘匿対象のデータの他、項目名や種類などのメタデータ(データベース的に言えば列)を持つ

  • キーチェーン要素は検索できる。キーチェーンを指定しないで検索した場合、securityd に登録されている全てのキーチェーンが検索対象になる

  • 要素単位でアクセス制御の設定ができる。設定内容としては以下がある

    • 全てのアプリケーションに要素の使用を許可

    • 特定のアプリケーションに要素の使用を許可

    • 許可する前にパスワード入力を要求

  • 要素へのアクセスが許可されているアプリケーションは、パスワード入力が不要になる。

    • アプリケーションがアクセス許可されておらず、かつプログラムからのパスワード入力もされていない場合、必要に応じてユーザにパスワード入力を求めるプロンプトを表示する。

  • アプリケーションからキーチェーンに要素を追加した場合、そのアプリケーションが自動的にアクセス許可される

アンロック・アクセス許可の要不要

後述のコードで実際の挙動を確認しつつ、キーチェーンまたは要素に対する操作ごとにアンロック状態(であること)・要素へのアクセス許可が必要かどうか表にまとめた。

対象

操作

アンロック状態

要素のアクセス許可

キーチェーン

開く( SecKeychainOpen )

ロック

アンロック

新規作成

(*1)

削除

設定変更

o

キーチェーン要素

検索(結果にメタデータ含む)

検索(結果に秘匿データ含む)

o

o

追加

o

更新(アクセス制御の変更)

o

o

削除

(*2)

  • (*1) キーチェーンの新規作成時はパスワード入力がセットで必要で、作成直後はアンロック状態になる

  • (*2) キーチェーンアクセスから要素を削除しようとするとアンロックが必要になるが、コードからだとロック状態でも削除できる(よくわからない)

  • アクセス許可が必要な操作では、アプリがアクセス許可されていてもアンロックは必要になる

注釈

削除がアンロック(パスワード入力)無しでできてしまうが、まぁ実態がファイルなのでどのみちファイルを消されたらどうしようもないというのがあると思われる。 5

検証コード

# XXX: 個人的な事情で Objective-C で書いたが、Swift の方が楽だった感がある

キーチェーンの作成

keychain-create.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainCreate("my-keychain", // pathName
                                        8,             // passwordLength
                                        "password",    // password
                                        FALSE,         // promptUser
                                        NULL,          // initialAccess
                                        &keychain);    // keychain

    if (status == errSecSuccess) {
        printf("Success\n");
    } else if (status == errSecDuplicateKeychain) {
        printf("errSecDuplicateKeychain: A keychain with the same name already exists.\n");
        return 1;
    } else {
        printf("error: %d\n", status);
        return 1;
    }

    CFRelease(keychain);
    return 0;
}

キーチェーンの削除

keychain-delete.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainOpen("my-keychain", &keychain);

    if (status == errSecSuccess) {
        printf("[OPEN] Success\n");
    } else {
        printf("[OPEN] error: %d\n", status);
        return 1;
    }

    status = SecKeychainDelete(keychain);
    if (status == errSecSuccess) {
        printf("[DELETE] Success\n");
    } else {
        printf("[DELETE] error: %d\n", status);
        return 1;
    }

    CFRelease(keychain);
    return 0;
}

キーチェーンのロック

keychain-lock.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainOpen("my-keychain", &keychain);

    if (status == errSecSuccess) {
        printf("[OPEN] Success\n");
    } else {
        printf("[OPEN] error: %d\n", status);
        return 1;
    }

    status = SecKeychainLock(keychain);

    if (status == errSecSuccess) {
        printf("[Lock] Success\n");
    } else {
        printf("[Lock] error: %d\n", status);
    }
}

キーチェーンのアンロック

keychain-unlock.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainOpen("my-keychain", &keychain);

    if (status == errSecSuccess) {
        printf("[OPEN] Success\n");
    } else {
        printf("[OPEN] error: %d\n", status);
        return 1;
    }

    status = SecKeychainUnlock(keychain, 8, "password", TRUE);

    if (status == errSecSuccess) {
        printf("[Unlock] Success\n");
    } else {
        printf("[Unlock] error: %d\n", status);
    }
}

キーチェーンの状態取得

keychain-getstatus.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainOpen("my-keychain", &keychain);

    if (status == errSecSuccess) {
        printf("[OPEN] Success\n");
    } else {
        printf("[OPEN] error: %d\n", status);
        return 1;
    }

    SecKeychainStatus keychainStatus = 0;
    status = SecKeychainGetStatus(keychain, &keychainStatus);
    if (status == errSecSuccess) {
        printf("[GetStatus] Success\n");

        // 定数が 1,2,4 で、 GetStatus の戻り値が 7 とかなので多分ビット列なんだと
        // 思われるが、ドキュメントに書いてない
        if (keychainStatus & kSecUnlockStateStatus) {
            printf("unlocked\n");
        }
        if (keychainStatus & kSecReadPermStatus) {
            printf("readable\n");
        }
        if (keychainStatus & kSecWritePermStatus) {
            printf("writable\n");
        }
    } else {
        printf("[GetStatus] error: %d\n", status);
    }
}

要素の追加

keychain-item-add.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainOpen("my-keychain", &keychain);

    if (status == errSecSuccess) {
        printf("[OPEN] Success\n");
    } else {
        printf("[OPEN] error: %d\n", status);
        return 1;
    }

    NSDictionary *query = @{
        (__bridge id)kSecValueData: [@"password" dataUsingEncoding: NSUTF8StringEncoding],
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecUseKeychain: (__bridge id)keychain,
        (__bridge id)kSecAttrLabel: @"label1", // 名前
        (__bridge id)kSecAttrAccount: @"account1",
        (__bridge id)kSecAttrService: @"service1",
    };
    status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);

    if (status == errSecSuccess) {
        printf("[SecItemAdd] Success\n");
    } else {
        printf("[SecItemAdd] error: %d\n", status);
    }

    CFRelease(keychain);
}

要素の削除

keychain-item-delete.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainOpen("my-keychain", &keychain);

    if (status == errSecSuccess) {
        printf("[OPEN] Success\n");
    } else {
        printf("[OPEN] error: %d\n", status);
        return 1;
    }

    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        // https://developer.apple.com/forums/thread/95762
        (__bridge id)kSecMatchSearchList: @[ (__bridge id)keychain ],
        (__bridge id)kSecReturnRef: @YES,
        (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne,
    };
    CFTypeRef result = NULL;
    status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);

    if (status == errSecSuccess) {
        printf("[SecItemCopyMatching] Success\n");

        status = SecKeychainItemDelete((SecKeychainItemRef)result);
        if (status == errSecSuccess) {
            printf("[SecKeychainItemDelete] Success\n");
        } else {
            printf("[SecKeychainItemDelete] error: %d\n", status);
        }

        CFRelease(result);

    } else if (status == errSecItemNotFound) {
        printf("[SecItemCopyMatching] NotFound\n");
    } else {
        printf("[SecItemCopyMatching] error: %d\n", status);
    }

    CFRelease(keychain);
}

要素の検索(結果にメタデータを含む)

keychain-item-list.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainOpen("my-keychain", &keychain);

    if (status == errSecSuccess) {
        printf("[OPEN] Success\n");
    } else {
        printf("[OPEN] error: %d\n", status);
        return 1;
    }

    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecReturnAttributes: @YES,
        // https://developer.apple.com/forums/thread/95762
        (__bridge id)kSecMatchSearchList: @[ (__bridge id)keychain ],
        (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll
    };
    CFTypeRef result = NULL;
    status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);

    if (status == errSecSuccess) {
        printf("[SecItemCopyMatching] Success\n");

        NSArray *ary = (__bridge_transfer NSArray *)result;
        printf("Search result: %ld\n", [ary count]);
        for (NSDictionary *item in ary) {
            NSLog(@"%@", item);
        }
        // CFRelease(result);

    } else if (status == errSecItemNotFound) {
        printf("[SecItemCopyMatching] NotFound\n");
    } else {
        printf("[SecItemCopyMatching] error: %d\n", status);
    }

    CFRelease(keychain);
}

要素の検索(結果に秘匿データ含む)

  • 結果に秘匿データを含める場合、アクセス許可が必要になる

  • 設計的に、秘匿データをアプリケーションのメモリ上に展開しなくても 認証等ができるようになっている?(ような雰囲気を感じる)

keychain-item-retrieve.m
#import <Foundation/Foundation.h>
#import <Security/Security.h>

int main(){
    SecKeychainRef keychain = NULL;

    OSStatus status = SecKeychainOpen("my-keychain", &keychain);

    if (status == errSecSuccess) {
        printf("[OPEN] Success\n");
    } else {
        printf("[OPEN] error: %d\n", status);
        return 1;
    }

    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecReturnAttributes: @YES,
        (__bridge id)kSecReturnData: @YES  // 秘匿データを結果に含める
        // https://developer.apple.com/forums/thread/95762
        (__bridge id)kSecMatchSearchList: @[ (__bridge id)keychain ],
        (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne,
    };
    CFTypeRef result = NULL;
    status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);

    if (status == errSecSuccess) {
        printf("[SecItemCopyMatching] Success\n");

        NSDictionary *item = (__bridge_transfer NSDictionary *)result;
        NSLog(@"%@", item);
        printf("password: %s\n", [[item objectForKey:@"v_Data"] bytes]);

    } else if (status == errSecItemNotFound) {
        printf("[SecItemCopyMatching] NotFound\n");
    } else {
        printf("[SecItemCopyMatching] error: %d\n", status);
    }

    CFRelease(keychain);
}

(おまけ) Makefile

Makefile
# Makefile
CC=gcc
CFLAGS=-Wall
FRAMEWORKS=-framework Foundation -framework Security
TARGETS=keychain-create keychain-delete keychain-getstatus keychain-item-add keychain-item-delete keychain-item-list keychain-item-retrieve keychain-lock keychain-unlock

all: $(TARGETS)

%: %.m
	$(CC) $^ -o build/$@ -lobjc -fobjc-arc $(FRAMEWORKS) $(CFLAGS)

個人的見解

  • まぁ、使うときはアンロックして使い終わったらロックという方針になるかなぁ

    • 要素ごとのアクセス制御があるのでロックしたからなんなんだという感があるが、以下の理由による

      • キーチェーンを適宜ロックしていれば大丈夫だった類の脆弱性とかあったらしい

      • 自動でロックされる・あるいは他アプリやユーザからロック可能な以上、セキュリティがどうのとは関係なく必要に応じてキーチェーンの状態を確認してアンロックする実装が必要になる

  • 預かり知らぬところで(パスワード不要で == 誰からも)削除される可能性があるので、適宜存在確認は必要そう

    • キーチェーンのデータが消えたら積む系の実装は避けた方が良さそう(必要なデータがなかったらエラーメッセージやガイドを表示する)

参考

Footnotes

1

Keychains | Apple Developer Documentation

2

キーチェーンアクセスに作成したキーチェーンを追加でき、GUIから設定を確認できる

  • 追加方法: キーチェーンアクセスのメニュー > ファイル > キーチェーンの追加

  • ( 正確には securityd の search list に追加していると思われるが )

3

Keychain data protection - Apple Support

4

man securityd にそれっぽいことが書いてある。

5

Secure Enclave に鍵を保存した場合は挙動変わる?(未検証)

DesktopApp keychain macOS

macOS でのキーチェーンの挙動 — ykrods note
https://www.ykrods.net/posts/2021/03/06/keychain-behavior-on-macos/

Comments