MacOS鑰匙串授權應用程序獲得密碼(命令行/Python/Objective-C/Swift)

MacOS鑰匙串授權應用程序獲得密碼

MacOS鑰匙串授權應用程序獲得密碼功能

MacOS自帶鑰匙串功能,可以安全地儲存密碼並自動輸入。還可以在需要時輕鬆查找密碼。當程序請求獲得密碼時,會出現如下提示框:

76fac8eb1a2d86eaf279fd50c387e85c.png

單擊拒絕按鈕將拒絕授權該應用程序獲得該密碼,單擊允許按鈕將授權該應用程序本次獲得該密碼,下次該應用程序嘗試獲得密碼時,仍將出現該對話框。單擊始終允許按鈕也將授權該應用程序本次獲得該密碼,區別在於下次該應用程序嘗試獲得密碼時不再出現該對話框。

單擊始終允許後,啓動鑰匙串訪問應用程序,在右上角的搜索框中搜索密碼項的名稱。

30179b2c497f941ecad2dbbb92c054fd.png

打開該密碼項。

27236d0e27ccfbb1e2c89618ce490a1d.png

然後切換到訪問控制

ba636938c5124c590dee1c32aba7b707.png

可以看到始終允許通過這些應用程序訪問列表中出現了前面請求獲得密碼的程序名稱security。在始終允許通過這些應用程序訪問下側有+加號和-減號。

選中始終允許通過這些應用程序訪問列表中的security,單擊-減號,將從始終允許通過這些應用程序訪問列表中刪除security項,單擊存儲更改按鈕,並在出現的鑰匙串訪問對話框中輸入鑰匙串的密碼。下次security應用程序嘗試獲得密碼時,將再次出現授權對話框。

單擊下側的+加號,將出現文件選擇對話框,在其中導航至本例的應用程序/usr/bin/security,單擊確定將把security添加到始終允許通過這些應用程序訪問列表中,單擊存儲更改按鈕,並在出現的鑰匙串訪問對話框中輸入鑰匙串的密碼。再次運行security程序請求獲得密碼時,不再出現授權提示框。

從操作流程來看,直觀的印象就是始終允許通過這些應用程序訪問列表的授權是應用程序的路徑/usr/bin/security,接下來做一下實驗。

複製security命令

先使用MacOS自帶的security命令獲取密碼,並按照上述操作授權。

/usr/bin/security find-generic-password -l "test_key" -gw

可以看到始終允許通過這些應用程序訪問列表中出現了程序名稱security

然後複製security命令到任意文件夾。

cp /usr/bin/security ./securityTest

運行命令獲取密碼。

./securityTest find-generic-password -l "test_key" -gw

會發現可以直接獲得密碼,並沒有出現授權提示框。

選中始終允許通過這些應用程序訪問列表中的security,單擊-減號,將從始終允許通過這些應用程序訪問列表中刪除security項,單擊存儲更改按鈕,並在出現的鑰匙串訪問對話框中輸入鑰匙串的密碼。

再次運行命令嘗試獲取密碼時,將出現授權提示框。

ed568b77ee943af15500245e756dff69.png

單擊允許按鈕將授權命令本次獲得該密碼,再次運行命令嘗試獲得密碼時,仍將出現該對話框。單擊始終允許按鈕也將授權命令本次獲得該密碼,再次運行命令嘗試獲得密碼時不再出現該對話框。打開該密碼項,然後切換到訪問控制,可以看到始終允許通過這些應用程序訪問列表中出現了前面請求獲得密碼的程序名稱security

be637b5c421cf068db9fdf292eaf1824.png

使用MacOS自帶的security命令獲取密碼。

/usr/bin/security find-generic-password -l "test_key" -gw

會發現可以直接獲得密碼,並沒有出現授權提示框。

選中始終允許通過這些應用程序訪問列表中的securityTest,單擊-減號,將從始終允許通過這些應用程序訪問列表中刪除securityTest項,單擊存儲更改按鈕,並在出現的鑰匙串訪問對話框中輸入鑰匙串的密碼。再次運行security命令嘗試獲取密碼時,將出現授權提示框。單擊下側的+加號,將出現文件選擇對話框,在其中導航至本例複製的命令securityTest,單擊確定將把security添加到始終允許通過這些應用程序訪問列表中,單擊存儲更改按鈕,並在出現的鑰匙串訪問對話框中輸入鑰匙串的密碼。再次運行security程序請求獲得密碼時,不再出現授權提示框。

從這兩個例子可以看出,鑰匙串授權應用程序並非是依賴應用程序的路徑,而是程序自身的某種簽名

使用python獲取密碼

接下來嘗試通過編程獲取密碼,爲了簡明直觀的敘述問題,選擇腳本語言python

儘管MacOS自帶python,爲對比起見,使用pyenv安裝pythonpyenv還包括命令python-build,稍後用於編譯python

首先安裝pyenv

brew install pyenv

然後使用pyenv安裝指定版本的python,本例中安裝版本3.8.2

pyenv install --verbose 3.8.2

安裝後查詢安裝的路徑

pyenv prefix 3.8.2

結果爲

~/.pyenv/versions/3.8.2

3.8.2版本中安裝用於訪問鑰匙串的keyring包。

~/.pyenv/versions/3.8.2/bin/pip install keyring

然後運行請求獲得密碼的程序。

~/.pyenv/versions/3.8.2/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

將出現如下提示框

6b4acb2b2f4c361d3e96303c0c3301fb.png

單擊允許按鈕將授權python本次獲得該密碼,再次運行python嘗試獲得密碼時,仍將出現該對話框。單擊始終允許按鈕也將授權python本次獲得該密碼,再次運行python嘗試獲得密碼時不再出現該對話框。打開該密碼項,然後切換到訪問控制,可以看到始終允許通過這些應用程序訪問列表中出現了程序名稱python3.8

691129622aa07b287caac73ff479ad02.png

使用另一個版本的python獲取密碼

接下來使用pyenv安裝另一個版本的python,本例中安裝版本3.8.1

pyenv install --verbose 3.8.1

安裝後查詢安裝的路徑

pyenv prefix 3.8.1

結果爲

~/.pyenv/versions/3.8.1

3.8.1版本中安裝用於訪問鑰匙串的keyring包。

~/.pyenv/versions/3.8.1/bin/pip install keyring

然後運行請求獲得密碼的程序。

~/.pyenv/versions/3.8.1/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

出現和前次3.8.2版本相同的提示框,前面已經授權3.8.2版本的python,再次出現提示框可見這兩個版本的python被視爲不同程序。

單擊允許按鈕將授權3.8.1版本的python本次獲得該密碼,再次運行3.8.1版本的python嘗試獲得密碼時,仍將出現該對話框。單擊始終允許按鈕也將授權3.8.1版本的python本次獲得該密碼,再次運行3.8.1版本的python嘗試獲得密碼時不再出現該對話框。打開該密碼項,然後切換到訪問控制,可以看到始終允許通過這些應用程序訪問列表中出現了兩個程序名稱python3.8

cb1df01cb361b1cc1a9209c21081864a.png

由此可見,儘管這兩個版本的python都顯示爲python3.8,然而鑰匙串視爲不同都應用程序需要分別授權。

複製python

接下來嘗試複製python

cp -r ~/.pyenv/versions/3.8.2 ~/.pyenv/versions/3.8.2-copy

由於始終允許通過這些應用程序訪問列表中顯示的兩個python3.8難以區分,爲避免混淆,清除始終允許通過這些應用程序訪問列表中已經授權的兩個程序。

重新授權給3.8.2版本的python

~/.pyenv/versions/3.8.2/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

然後運行復制的3.8.2版本的python

~/.pyenv/versions/3.8.2-copy/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

沒有出現授權提示框,查看密碼項的始終允許通過這些應用程序訪問列表會看到只有一個3.8.2,說明仍舊只授權了一次,複製的3.8.2版本的python是按照前一次的授權獲得密碼的。

從這個測試也可以看到是按照程序而不是路徑授權的。

再次編譯python

既然複製文件被視爲相同,那麼編譯相同的版本呢?保留前面安裝的3.8.1版本,再次編譯一遍,直接使用安裝pyenv時自帶的命令python-build

python-build 3.8.1 ~/.pyenv/versions/3.8.1-build2

在第二次編譯的3.8.1版本中安裝用於訪問鑰匙串的keyring包。

~/.pyenv/versions/3.8.1-build2/bin/pip install keyring

先運行第一次編譯的3.8.1版本。

~/.pyenv/versions/3.8.1/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

然後運行第二次編譯的3.8.1版本。

~/.pyenv/versions/3.8.1-build2/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

出現授權提示框,說明即使使用相同源代碼編譯,也會因爲編譯環境的區別導致被視爲不同程序。

使用相同路徑編譯python

先移動第一次編譯的程序

mv ~/.pyenv/versions/3.8.1 ~/.pyenv/versions/3.8.1-build1

使用移動後的程序運行。

~/.pyenv/versions/3.8.1-build1/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

沒有出現授權提示框,使用和之前相同的命令安裝3.8.1版本。

pyenv install --verbose 3.8.1

在新安裝的3.8.1版本中安裝用於訪問鑰匙串的keyring包。

~/.pyenv/versions/3.8.1/bin/pip install keyring

然後在新安裝的3.8.1版本中運行請求獲得密碼的程序。

~/.pyenv/versions/3.8.1/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

出現授權提示框,經過二進制比較會發現,由於程序中包含編譯時的臨時文件夾信息,兩次編譯的臨時文件夾路徑不同,儘管目標路徑相同,文件也不相同。

下面用第一次編譯的可執行程序覆蓋第二次編譯的可執行程序。

cp ~/.pyenv/versions/3.8.1-build1/bin/python ~/.pyenv/versions/3.8.1/bin/python

然後在新安裝的3.8.1版本中用第一次編譯的可執行程序運行請求獲得密碼的程序。

~/.pyenv/versions/3.8.1/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

沒有出現授權提示框。

從這個例子可以看出,鑰匙串校驗的是嘗試獲取密碼的可執行程序,而不是路徑。

本例中校驗的是python文件,當鑰匙串授權python可以獲得指定密碼時,任何程序調用python都可以獲得該密碼。甚至並不需要調用相同位置的python程序,只需要python可執行程序相同,即可通過鑰匙串的校驗。如果兩個程序調用相同的python可執行程序獲得所需的密碼,那麼也可以獲得另一個程序的密碼。

編譯成共享庫

當應用程序中的APP嘗試獲得密碼時,如果選擇始終允許始終允許通過這些應用程序訪問列表中出現的是APP的名稱,把python編譯成共享庫將得到相同效果。

sudo env PYTHON_CONFIGURE_OPTS="--enable-framework" python-build 3.8.2 ~/.pyenv/versions/3.8.2-framework

在重新編譯的3.8.2版本中安裝用於訪問鑰匙串的keyring包。

sudo ~/.pyenv/versions/3.8.2-framework/bin/pip install keyring

在密碼項中清空始終允許通過這些應用程序訪問列表,然後在重新編譯的3.8.2版本中運行獲取密碼的腳本。

~/.pyenv/versions/3.8.2-framework/bin/python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

出現授權提示框,單擊始終允許按鈕授權後,打開該密碼項,然後切換到訪問控制,可以看到始終允許通過這些應用程序訪問列表中出現了不同於前面的程序名稱Python.app

05b99a23c7caac453d64947d35671731.png

這是因爲編譯時使用了PYTHON_CONFIGURE_OPTS="--enable-framework"環境變量,該環境變量使python-build編譯時添加--enable-framework參數。

不包含--enable-framework參數編譯的目錄結構爲

  • ~/.pyenv/versions/3.8.2
    • ~/.pyenv/versions/3.8.2/bin
    • ~/.pyenv/versions/3.8.2/include
    • ~/.pyenv/versions/3.8.2/lib
    • ~/.pyenv/versions/3.8.2/share

包含--enable-framework參數編譯的目錄結構爲

  • ~/.pyenv/versions/3.8.2-framework
    • ~/.pyenv/versions/3.8.2-framework/bin -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current/bin -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/bin
    • ~/.pyenv/versions/3.8.2-framework/bin.orig -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current/bin -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/bin
    • ~/.pyenv/versions/3.8.2-framework/include -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current/include -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/include
    • ~/.pyenv/versions/3.8.2-framework/lib -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current/lib -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/lib
    • ~/.pyenv/versions/3.8.2-framework/Python.framework
      • ~/.pyenv/versions/3.8.2-framework/Python.framework/Headers -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current/include/python3.8 -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/include/python3.8
      • ~/.pyenv/versions/3.8.2-framework/Python.framework/Python -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current/Python -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Python
      • ~/.pyenv/versions/3.8.2-framework/Python.framework/Resources -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current/Resources -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources
      • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions
        • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8
          • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/bin
          • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Headers -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/include/python3.8
          • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/include
            • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/include/python3.8
          • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/lib
          • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Python
          • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources
            • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/English.lproj
            • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Info.plist
            • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python.app
          • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/share
        • ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8
    • ~/.pyenv/versions/3.8.2-framework/share -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/Current/share -> ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/share

密碼項的始終允許通過這些應用程序訪問列表中出現的程序名稱Python.app,即爲編譯中的~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python.app

始終允許通過這些應用程序訪問列表中刪除Python.app,然後運行腳本,會看到授權提示框。單擊始終允許通過這些應用程序訪問列表下側的+加號,添加~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python.app,再次運行腳本,不出現授權提示框。由此可見,始終允許通過這些應用程序訪問列表不僅支持可執行文件,也支持APP程序包。

打開程序包~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python.app可以看到裏面包含的文件,直接執行其中的可執行文件。

~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python.app/Contents/MacOS/Python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));' 

可以獲得密碼而不出現授權提示框,說明程序包中的可執行文件也是使用前面對~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python.app的授權。

複製程序包。

sudo cp -r ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python.app ~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python-copy.app

然後運行復制的程序包中的可執行程序。

~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python-copy.app/Contents/MacOS/Python -c 'import keyring; print(keyring.get_password("test_key", "test_username"));'

可以獲得密碼而不出現授權提示框,說明覆制的程序包中的可執行文件也是使用前面對~/.pyenv/versions/3.8.2-framework/Python.framework/Versions/3.8/Resources/Python.app的授權。

打包成獨立文件

從前面的共享庫案例可以看出,授權給Python.app後,可以任意複製已經獲得授權的Python.app。結合前面幾個案例,當用戶在一個場景中授權一個可執行程序或者APP獲取密碼後,如果另一個場景中也調用相同可執行程序或者APP嘗試獲取相同的密碼,不會出現授權提示框。

既然像python可執行程序並不能限制授權後僅獲得指定的密碼,因此,應當避免使用類似於Python.app這樣的公共APP獲取密碼,把獲取密碼的腳本打包成獨立的文件即可在一定程度上避免這種情況。

接下來使用pyinstaller打包成獨立文件,在重新編譯的3.8.2版本中安裝用於訪問鑰匙串的pyinstaller包。

sudo ~/.pyenv/versions/3.8.2-framework/bin/pip install pyinstaller

創建文件寫入前面的腳本。

code get_password.py

文件內容爲:

import keyring;
import keyring.backends.OS_X;
keyring.set_keyring(keyring.backends.OS_X.Keyring());
print(keyring.get_password("test_key", "test_username"));

使用pyinstaller打包成獨立文件:

~/.pyenv/versions/3.8.2-framework/bin/pyinstaller --onefile ./get_password.py

打包後運行

./dist/get_password

會出現如下提示框:

5dba57e6791714ba49fbf5f000354a1c.png

從提示框中可以看出,請求授權的程序是打包成的獨立文件get_password,而不是打包進的python,選擇始終允許,打開密碼項,可以看到始終允許通過這些應用程序訪問列表中出現的也是打包成的獨立文件get_password的名稱,

646cfc863a4b16b69d07cd82656cbee3.png

打包中如果遇到異常,可以使用如下命令清除臨時文件。

trash ./__pycache__ ./build ./dist ./get_password.spec

使用Objective-C獲得密碼

儘管使用pyinstaller打包成獨立文件能在一定程度上避免前述的問題,不過打包的體積較大,而且運行速度也較慢。此時可以直接使用Objective-C獲得密碼。

//
//  main.m
//  FindGenericPassword
//
//  Created by 胡爭輝 on 2020/5/26.
//  Copyright © 2020 胡爭輝. All rights reserved.
//

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString* service = @"test_key";
        NSString* account = @"test_username";
        UInt32 pwLength = 0;
        void* pwData = NULL;
        SecKeychainItemRef itemRef = NULL;
        OSStatus status = SecKeychainFindGenericPassword(
                                                         NULL,
                                                         (UInt32) service.length,
                                                         [service UTF8String],
                                                         (UInt32) account.length,
                                                         [account UTF8String],
                                                         &pwLength,
                                                         &pwData,
                                                         &itemRef);
        if (status == errSecSuccess) {
            NSData* data = [NSData dataWithBytes:pwData length:pwLength];
            NSString* password = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            printf("%s\n", [password UTF8String]);
        }
        if (pwData) SecKeychainItemFreeContent(NULL, pwData);
    }
    return 0;
}

使用Swift獲得密碼

Swift代碼如下

//
//  main.swift
//  FindGenericPasswordSwift
//
//  Created by 胡爭輝 on 2020/5/26.
//  Copyright © 2020 胡爭輝. All rights reserved.
//

import Foundation

var service:String = "test_key"
var account:String = "test_username"
var pwLength:UInt32 = 0;
var pwData:UnsafeMutableRawPointer? = nil;
var item:SecKeychainItem? = nil;

var status:OSStatus = SecKeychainFindGenericPassword(
    nil,
    UInt32(service.lengthOfBytes(using: String.Encoding.utf8)),
    service.cString(using: String.Encoding.utf8),
    UInt32(account.lengthOfBytes(using: String.Encoding.utf8)),
    account.cString(using: String.Encoding.utf8),
    &pwLength,
    &pwData,
    &item)
if (status == errSecSuccess) {
    if let myData = pwData {
        let password:String? = String.init(bytesNoCopy: myData, length: Int(pwLength), encoding: String.Encoding.utf8, freeWhenDone: true)
        if let myPassword = password {
            print(myPassword)
        }
    }
}

代碼簡單調用API實現。

運行程序會出現如下提示框:

0ee9b8ce3d418c953485ea64aff85ea2.png

從提示框中可以看出,請求授權的程序是打包成的獨立文件FindGenericPasswordSwift,選擇始終允許,打開密碼項,可以看到始終允許通過這些應用程序訪問列表中出現的也是打包成的獨立文件FindGenericPasswordSwift的名稱,

6dff6f847696ab5b35cf03333e4ab232.png

默認設置情況下,該可執行程序位於類似於如下的路徑

~/Library/Developer/Xcode/DerivedData/FindGenericPasswordSwift-ddztoauqdiyyrpgystayydxtibil/Build/Products/Debug/FindGenericPasswordSwift

單獨刪除可執行文件,重新編譯運行,沒有出現提示框,說明編譯出來的程序相同。

選擇XCodeProduct菜單,Clean Build Folder菜單項清理構建文件夾,再次編譯運行,將再次出現提示框,選擇始終允許,打開密碼項,可以看到始終允許通過這些應用程序訪問列表中出現了兩個打包成的獨立文件FindGenericPasswordSwift的名稱,

1ce2b43d8ea8baced56cc9bd55542c7d.png

可執行文件的路徑仍舊相同,說明重新編譯的可執行文件不同,所以需要重新授權。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章