MacOS钥匙串授权应用程序获得密码
文章目录
MacOS
钥匙串授权应用程序获得密码功能
MacOS
自带钥匙串功能,可以安全地储存密码并自动输入。还可以在需要时轻松查找密码。当程序请求获得密码时,会出现如下提示框:
单击拒绝
按钮将拒绝授权该应用程序获得该密码,单击允许
按钮将授权该应用程序本次获得该密码,下次该应用程序尝试获得密码时,仍将出现该对话框。单击始终允许
按钮也将授权该应用程序本次获得该密码,区别在于下次该应用程序尝试获得密码时不再出现该对话框。
单击始终允许
后,启动钥匙串访问
应用程序,在右上角的搜索框中搜索密码项的名称。
打开该密码项。
然后切换到访问控制
。
可以看到始终允许通过这些应用程序访问
列表中出现了前面请求获得密码的程序名称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
项,单击存储更改
按钮,并在出现的钥匙串访问
对话框中输入钥匙串的密码。
再次运行命令尝试获取密码时,将出现授权提示框。
单击允许
按钮将授权命令本次获得该密码,再次运行命令尝试获得密码时,仍将出现该对话框。单击始终允许
按钮也将授权命令本次获得该密码,再次运行命令尝试获得密码时不再出现该对话框。打开该密码项,然后切换到访问控制
,可以看到始终允许通过这些应用程序访问
列表中出现了前面请求获得密码的程序名称security
。
使用MacOS
自带的security
命令获取密码。
/usr/bin/security find-generic-password -l "test_key" -gw
会发现可以直接获得密码,并没有出现授权提示框。
选中始终允许通过这些应用程序访问
列表中的securityTest
,单击-
减号,将从始终允许通过这些应用程序访问
列表中删除securityTest
项,单击存储更改
按钮,并在出现的钥匙串访问
对话框中输入钥匙串的密码。再次运行security
命令尝试获取密码时,将出现授权提示框。单击下侧的+
加号,将出现文件选择对话框,在其中导航至本例复制的命令securityTest
,单击确定将把security
添加到始终允许通过这些应用程序访问
列表中,单击存储更改
按钮,并在出现的钥匙串访问
对话框中输入钥匙串的密码。再次运行security
程序请求获得密码时,不再出现授权提示框。
从这两个例子可以看出,钥匙串授权应用程序并非是依赖应用程序的路径,而是程序自身的某种签名
使用python
获取密码
接下来尝试通过编程获取密码,为了简明直观的叙述问题,选择脚本语言python。
尽管MacOS
自带python,为对比起见,使用pyenv安装python,pyenv还包括命令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"));'
将出现如下提示框
单击允许
按钮将授权python本次获得该密码,再次运行python尝试获得密码时,仍将出现该对话框。单击始终允许
按钮也将授权python本次获得该密码,再次运行python尝试获得密码时不再出现该对话框。打开该密码项,然后切换到访问控制
,可以看到始终允许通过这些应用程序访问
列表中出现了程序名称python3.8
使用另一个版本的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
由此可见,尽管这两个版本的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
。
这是因为编译时使用了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
会出现如下提示框:
从提示框中可以看出,请求授权的程序是打包成的独立文件get_password
,而不是打包进的python,选择始终允许
,打开密码项,可以看到始终允许通过这些应用程序访问
列表中出现的也是打包成的独立文件get_password
的名称,
打包中如果遇到异常,可以使用如下命令清除临时文件。
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
实现。
运行程序会出现如下提示框:
从提示框中可以看出,请求授权的程序是打包成的独立文件FindGenericPasswordSwift
,选择始终允许
,打开密码项,可以看到始终允许通过这些应用程序访问
列表中出现的也是打包成的独立文件FindGenericPasswordSwift
的名称,
默认设置情况下,该可执行程序位于类似于如下的路径
~/Library/Developer/Xcode/DerivedData/FindGenericPasswordSwift-ddztoauqdiyyrpgystayydxtibil/Build/Products/Debug/FindGenericPasswordSwift
单独删除可执行文件,重新编译运行,没有出现提示框,说明编译出来的程序相同。
选择XCode
的Product
菜单,Clean Build Folder
菜单项清理构建文件夹,再次编译运行,将再次出现提示框,选择始终允许
,打开密码项,可以看到始终允许通过这些应用程序访问
列表中出现了两个打包成的独立文件FindGenericPasswordSwift
的名称,
可执行文件的路径仍旧相同,说明重新编译的可执行文件不同,所以需要重新授权。