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

可执行文件的路径仍旧相同,说明重新编译的可执行文件不同,所以需要重新授权。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值