keyring是怎么工作的——基础篇

​ 优秀的软件总是能化繁为简,以精巧的方式把工作完成的同时,还能给人以极简的使用体验,今天的主角gnome-keyring(简称keyring,后同)就属于这么一类软件。

​ keyring是怎么工作的?简单来说,keyring就是一个密码(以及秘钥等)的管理系统,甚至在你毫无察觉的情况下(当然,这是在不出意外的情况下)帮你完成密码的存储、加密、获取和更新等。普通用户知道这些就够了,因为复杂的事情keyring都已经帮你做了,你只要享受keyring带来的便利即可。但是对于一个开发者——尤其是deepin开发者——来说,仅仅了解这些显然是远远不够的,我们需要了解更多。

​ 探索keyring之前,我们要先了解其中的一些基础概念。因为目前gnome-keyring官方推荐使用libsecret以将keyring服务集成到应用程序中去,所以以下概念均以libsecret为准,libgnome-keyring中有能对应之的,也会列出来方便大家看到旧文档(大部分文档都比较旧)涉及相关名词的时候可以一一对应。

名词表

名词 (libsecret) 解释 旧称 (libgnome-keyring)
SecretCollection 密码项的集合,每一个密码对都要存到一个SecretCollection中。每个Collection都有自己的主密码,Collection被解开后,其中包含的密码项就可以被访问到。 Keyring
SecrectItem 一个密码项 KeyringItem
SecretAttributes 密码项附属的其他属性,方便查找和增加密码维度 ItemAttributes
SecretSchema 一类密码关联的属性集定义

(注:libsecret是libgnome-keyring的继任者)

​ 这里可以简单地把SecretCollection认为是SecretItem的容器,不同的SecretCollection作用不同:

​ 在keyring的世界里,一共有三种类型的SecretCollection:login、session 和 自定义collection,其中login collection和session collection被官方定义为well-known,之所以well-known是因为他们比较特殊:

  • login collection会在用户登录的时候自动解锁,此时应用程序就可以方便的存取自己帮用户保存在keyring中的密码,不会惊扰到用户一丝一毫,毕竟keyring的设计目标是为用户提供平滑的密码和秘钥UI体验;
  • session collection则只会在用户会话内保存(内存中),用户退出会话,所有保存在session collection中的密码都会消失;

事实上,只有login collection和我们的关系最为紧密,keyring出问题和不出问题都有它的事儿,因为gnome-keyring中默认的collection就是login collection。

​ 应用自己创建collection这种情况则相对较少(我没有见到过),我觉得主要是是因为自有的collection在解锁的时候,会弹出密码提示框,让用户填入密码,虽然相对更安全了一点,但是却对用户造成了极大的干扰。例如你随便在网上搜一下就可以看到多少人在抱怨。当然这个搜索结果中的例子并不是因为使用了自有的collection,但是多少能说明点问题。

​ SecretAttributes和SecretSchema从字面上即可理解其意图:为密码项添加额外的属性一方面,一类密码最好有统一的属性,方便搜索;另一方面,我觉得可以把密码的存储进行细化分层,方便应用扩展。

架构图

​ 我们针对keyring的主要操作其实就是对密码项的CRUD(增、查、改、删),看起来操作很简单,就像Redis和memcached一样,起初一看不就是个map么,操作简单的令人发指,但是等真正了解其设计和应对的场景后,才知道其内部是何其复杂。

​ gnome-keyring的架构也比较复杂,详细可以查看官方原图 。我这里进行了简化,只保留四个最为主要的部分:Prompt UI、Library、 PAM、Daemon,架构简图如下:

keyring-architecture

(注:详细版本架构图请看原图 )

​ 其中,gnome-keyring-daemon是keyring最主要的实现部分,提供包括密码加密和存储、DBus API、认证等等,其在大部分情况下已经工作得相当好了,如果你不是密码学爱好者或者专家,就不需要细致研究了。

​ 这里只简单提一下PKCS#11,之前一直以为它是一种专用的证书或者密码机构,因为在生成pem文件的时候会用到一个类似的 pkcs12模块(被我当成了pkcs11的升级),但其实按照Wikipedia它是:

PKCS#11标准定义了与密码令牌(如硬件安全模块(HSM)和智能卡)的独立于平台的API,并将API本身命名为“Cryptoki”(来自“加密令牌接口”,发音为“crypto-key” - 但是“PKCS#11”通常用于指代API以及定义它的标准)。 API定义了最常用的加密对像类型(RSA密钥,X.509证书,DES / 三重DES密钥等)以及使用,创建/生成,修改和删除这些对象所需的所有功能。

​ 跟我的想象相差甚远。

​ Library部分指的是keyring提供给开发者的库或者开发工具,目前主要是 libgnome-keyring 和 gnome-keyring的命令行工具,而且libgnome-keyring目前已经废弃好长时间了。

​ 再看Prompt UI部分,keyring还有UI?答案是肯定的!而且并不是seahorse,seahorse只是一个keyring配套的GUI管理程序,方便用户对keyring进行一些直观的操作。这里说得keyring的Prompt UI其实是那个大家都很讨厌的恼人对话框:

keyring-prompt-ui

为什么有时候这个对话框莫名其妙会弹出来?先卖个关子。

​ 上面还剩下一个重要的部分没说,其实就是PAM。它有两个作用,一个是自动解锁、一个是密码更新。

自动解锁

​ gnome-keyring提供了一个pam模块pam_gnome_keyring.so,用户在登录系统的时候,这个pam模块会尝试启动gnome-keyring-daemon,完成后进入“潜伏”状态:用户开始输入密码,输入了半天,“啪”,输错了……这个pam模块就什么也不做;直到用户完成认证,这个pam模块就会拿着用户密码透过gnome-keyring-daemon的接口解开login collection!这个时候login collection不存在也不要紧,login collection会先被创建,然后被解锁!然后应用就可以如愿使用之前存储在keyring中的密码了。当然,这是正常情况。

​ 如果用户不输入密码怎么办?也就是如果用户是自动登录的,会怎么样?答案显而易见,但是也是沮丧的,如果你打开一个需要keyring密码的应用,例如chrome,上面那个烦人的弹框就会出现……这还不是最严重的情况,这种情况下,用户还可以通过关闭自动登录解决问题。但是假如这个用户是第一次登录系统,还没有default collection即login collection,keyring会创建一个叫default的collection(注意虽然之前login collection是default collection,但是它并不叫default!)并且弹出对话框让用户给这个collection设定一个密码。这会导致就算是用户某天想开了,关闭了自动登录,pam也不会去尝试解开这个collection(pam只会解锁login collection),也就是每次用户都会被弹这个对话框……

​ 这就是迷之对话框产生的前两种情况。

密码更新

​ 上面说了gnome-keyring自带了一个pam_gnome_keyring.so的pam模块,这个模块除了会在用户登陆的时候解开login collection外,它的另外一个作用是在用户更新密码的时候将login collection的密码进行更新!是不是很贴心?

​ 那么问题又来了,假如你用了类似sudo passwd USER这样的命令进行密码更新,或者使用DE提供的更改密码的GUI修改密码,而它的实现也是在root级别,那么很遗憾你的login collection密码是不会被更新的,因为这种情况下的pam认证是对root进行认证,而不是你个人!你个人的密码是被成功更改了,但是login collection的并没有啊,这种情况下,pam也就无法在登录时帮你解锁,你也就又要被弹框了……

​ 还有一种类似的情况,假如你修改密码的方式甚至没有通过pam,与上面一样,你还是要被弹框……

​ 所以说,简简单单用 passwd USER 就好了,省心!

keyring卡住了我的应用

​ 上面只是提到了gnome-keyring-daemon是被pam_gnome_keyring.so拉起来的,但是pam运作的阶段很早,这时候你的用户会话压根还没有建立(注意用户会话和pam的会话是两个不同的概念),gnome-keyring-daemon想要建立DBus通信也是不可能的。这时候就要靠keyring自带的几个自启动文件了(/etc/xdg/autostart/gnome-keyring-*.desktop),这几个自启动文件也会启动gnome-keyring-daemon,不过并不会建立新的进程,而是对之前启动的gnome-keyring-daemon进行不同方面的“初始化”,很多会话相关的信息都是通过这几个“兄弟”传递过去的。

​ 这些信息中就包括DBUS_SESSION_BUS_ADDRESS这个环境变量,这对依赖DBus的程序至关重要啦。而deepin对keyring启动的一些列调整(见下篇),造成的结果是这个环境变量并没有被正常传递过去。没有这个环境变量,keyring就无法声明DBus接口了,然后用到keyring的程序,例如chrome,在启动的时候就卡住了……

​ 这就是“chrome卡住了”事件的前因后果。

​ 另外,值得一提的是,gnome-keyring-daemon这个进程被启动了很多次,“单实例”主要是靠一个叫GNOME_KEYRING_CONTROL的环境变量,gnome-keyring-daemon实例启动的时候会通过各种方式搜寻这个信息(有时候可能还没有以环境变量的形式展示),一旦发现已经有“兄弟”启动了,就会为这个“兄弟”“两肋插刀”——挂掉……

​ 想想也是挺惨的。

下篇预告:keyring是怎么工作的——和polkit的交互

参考链接

2 条思考于 “keyring是怎么工作的——基础篇

    1. wangyaohua 文章作者

      管理你的密码用的,像chrome不是会帮你存储用户名和密码么,其实在Linux下就是存到keyring里面的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注