*本文原创作者:mscb,本文属FreeBuf原创奖励计划,未经许可禁止转载
密码管理器大家都有用过吧?但你相信有一种密码管理器不仅可以跨设备使用,还无需同步!无论你相不相信,今天就让我来教你如何自己动手制作一个。
密码管理器,实在太多。简直能让人挑花眼。有各种各样类型的,但主要是有2种:网络同步的、存在自己本地数据库的。从理论上讲,存在本地的会比自己存在网络上的会相对安全一点。因为最近经常能听到某些网络型密码管理器被爆出漏洞什么的,虽然说你保存的密码是加密保存在服务器上的,但还是有点安全隐患。
相对于网络型密码管理器,本地型密码管理器确实会安全一点(取决与你的安全意识)。不过,与此同时,也带来一些缺点。比如没有网络型的方便,需自己对保存密码的数据库进行同步等等。
跨平台?离线?不同步?
如果要实现这3个要求,我们需要重新审视一下自己对应密码管理器的需求。首先,我们使用密码管理器的目的不仅仅是为了保存密码,还为了安全。保证每个网站注册的密码都不同,而且是随机的,不存在与本人有挂钩的信息,比如手机号、生日、幸运数字等等,防止他人根据你的个人信息生成字典跑包。
既然要保证密码中没有个人信息,而且需要无序,随机数显然是最好的选择。但如果使用随机数就无法实现每个平台都相同。所以必须使用一个统一的算法,这样各个设备使用同一套算法就能保证生成的密码都是相同的。
最终,想到如下算法,来实现密码同步:
用户需指定一个通用密码
需要指定一个网站的域名(不包含具体文件地址)
使用Hash算法生成密码。
这样,我们就能保证各个网站的密码都不一样,而且还不需要同步!
密码管理器最终还是为浏览器服务的,不如我们直接将它做成Chrome浏览器扩展,这样使用方便,还能跨平台。
开发一个chrome扩展其实很简单,只需要一点js的知识就行了,接下来让我们一起来看看如何开发!
要开发,首先需要 开发者文档 ,这是Google官方的文档地址,请自备梯子。如果需要中文文档的话可以自己找翻译版本的~
第一,在本地新建一个文件夹,这里取名为“ImPassword”
第二,新建一个名称为 manifest.json
的文件,这是用于保存整个扩展的描述信息。
{ "manifest_version": 2, "name": "扩展名称", "description": "扩展描述文件", "version": "版本号", "browser_action": { "default_icon": "默认图标", "default_popup": "点击弹出的HTML网页" }, "permissions": [ "权限信息" ] }
第三,我们再来分析一下。我们这个扩展的需求。
1.扩展要能提取到浏览器地址栏的网址。
2.能储存必要的信息到本地中。
3.对所有的网站都必须要有读取的权限。
第四,根据需求,我们查查开发者文档,需要加入一些权限,比如: "permissions": ["tabs","storage","<all_urls>"]
第五,把最终的 manifest.json
文件写出来!
{ "name": "密码管理器", "manifest_version":2, "version": "1.0", "description": "密码管理器Chrome扩展", "permissions": [ "tabs", "storage", "<all_urls>" ], "options_page": "options.html", "browser_action": { "default_icon": "icon.png", "default_popup": "popup.html" }, "background" : { "scripts" : [ "bg.js" ] } }
这里简单的对上面的信息解析说明。 permissions
表示你需要申请的权限。 options_page
表示“选项”界面和html网页,也就是说,有了这个字段则当扩展安装到浏览器后,能实现 右键->选项 操作。 background
里的内容我这里是添加一个空js文件,因为如果没有该参数,每次点开扩展的时候鼠标会一直转转转,强迫症患者表示无法接受。
第六,根据信息,建立相关的文件。
从用户的角度看,用户点击一下我们的扩展图标会弹出一个小小的框框,而这个框实际上就是我们上面配置的 popup.html
。使用这个html文件里的内容就是用户能看到的东西,可以称它为显示界面。
对于 options.html
内的内容则是当用户 右键->选项
时显示的内容。我们现在的目的就是,通过选项界面需要用户配置一个独立密钥,并保存。而当用户点击扩展图标的时候,扩展会自动获取网站的域名,并与设置的密钥进行Hash操作,产生一个重复率比较低的网站密码。
首先,打开 popup.html
文件,写入以下内容
<html> <body> <p id="status"> hello </p> </body> <script type="text/javascript" src="/md5.js"></script> <script type="text/javascript" charset="utf-8" src="/popup.js"></script> </html>
这里的代码实在太简单了,相信大家都能看懂。id为status的标签主要用来显示网站的密码,下面引用了2个JS文件,一个是我们等一下要写的 popup.js
文件,还有一个是用于哈希的库。
由于考虑到很多网站的密码位数不能大于20位,大部分的hash函数都无法用,这里只能用一个16 BIT MD5的hash算法。
现在,先来写一下选项界面的代码。
打开options.html,写入以下内容。
<!DOCTYPE html> <html> <head><title>选项</title></head> <body> <div>输入密码: <input type="text" id="text"></input> <button id="set">保存</button> </div> <script src="options.js"></script> </body> </html>
其实也就是一个简单的html,一个文本框一个按钮而且。接下来打开options.js,开始真正写代码了。
document.body.onload = function() { chrome.storage.sync.get("data", function(items) { if (items.data == undefined){ return; } if (!chrome.runtime.error) { console.log(items); document.getElementById("text").value = items.data; } }); } document.getElementById("set").onclick = function() { var d = document.getElementById("text").value; chrome.storage.sync.set({ "data" : d }, function() { if (chrome.runtime.error) { console.log("Runtime error."); } }); window.close(); }
下面来和分析一下,上面代码的具体意思。代码由2个块组成,这2个块实际上都是事件。第一个为载入事件(即当你打开选项界面的时候会触发的内容),第二个为ID为set的按钮的点击事件。
chrome.storage.sync.get
是chrome的一个api,用于获取储存的内容。这里的判断逻辑是,如果获取有获取到内容(即说明之前有保存过密码),则显示原先的密码。如果没有获取到,就不显示。
chrome.storage.sync.set
就很好理解了,当然是把密码保存起来起来。
既然选项界面的代码写好了,接下来看看popup.js里的内容,这个是这个扩展的主要逻辑部分。但也不要担心,这个扩展只是做了一点微小的工作,不会很难~
看看popup.js的全部代码,等一下我们再来分析具体的原理。
var receiver_data = new Array(), callFun; chrome.tabs.query({'active': true, 'windowId': chrome.windows.WINDOW_ID_CURRENT}, function(tabs){ var urlLink = tabs[0].url.replace("http://", '').replace("https://", '').split('/')[0]; chrome.storage.sync.get("data", function(items) { if(items.data == undefined){ document.getElementById('status').textContent = "未配置独立密钥!"; return; } if (!chrome.runtime.error) { pass = items.data; receiver(["Password",pass]); //发送信号 } }); receiver(["Url",urlLink]); receiver(["RUN",run]);//发送函数信号 } ); function receiver(value){ //信号接受函数 if(value[0] == "RUN"){ callFun = value[1]; return 0; } if(receiver_data.length >= 2){//清空 receiver_data.length = 0; receiver_data.push(value); return 0; } if(receiver_data.length <= 2){//继续添加 receiver_data.push(value); } if(typeof(callFun)=="function" && receiver_data.length == 2){ callFun(receiver_data); } } function run(buf){ if(buf.length == 2){ var pass,urlad; if(buf[0][0] == "Url"){ urlad = buf[0][1]; pass = buf[1][1]; }else{ pass = buf[0][1]; urlad = buf[1][1]; } document.getElementById('status').textContent = (urlad.MD5()+pass.MD5()).MD5(); } }
开头定义了2个全局变量,一个是接收器数组,一个是储存函数指针的变量。
由于Chrome是以回调的方式返回具体的url的,这样会给我们处理带来很大不便。所以 receiver
是我自己定义的一个接收器,用于处理传入的信号,并将信号数据保存到接收器数组里。接收器数组就像一个容器,接受到的数据只要不超过数组下标,均保存起来。接收器会对传入的信号进行处理,如果没出现Run标志,则把数据保存到数组中,如果出现指定标志,则通过函数指针执行指定函数,同时带上接收器数组的内容。
介绍完接收器的具体实现方式,我们再来看看,这个代码的具体逻辑。
由于我们需要得到tab的URL地址,并对其进行分解。所以要使用Chrome的一个api去实现(前面我们已经申请了权限)。
chrome.tabs.query({'active': true, 'windowId': chrome.windows.WINDOW_ID_CURRENT},function(tabs){})
现在只需要在 function(tabs){}
内写入需要执行的操作即可!而该匿名函数内的处理的是,对tabs得到的url地址进行分解,得到一个域名地址,并保存到urllink中。接着从chrome.storage取到之前保存的密钥,如果没有则显示“未配置密钥”。然后就是把密码、地址、run函数指针,通通发送给receiver.
receiver
判断传入的标签,如果是RUN则将函数指针保存到callFun变量中,如果是其他的则保存到receiver_data数组里。
run
函数主要是处理由receiver传的参数,并对参数解析。最终进行Hash计算并输出到ID为status的标签中。
虽然说,这里使用的密码是用md5 16bit 计算出来的,但也确实是无奈之举,同时也不算太安全。但相对与N个网站用同一个密码来说,这套不需要同步的且N个网站对应N个密码的方案,确实能有效的提高安全性。不过,这套方案真的有那么完美吗?显然不是。举个例子,你无法很高效更改某个网站的密码。假设有一天你注册过的某个网站被脱裤,你的密码被爆出来了,虽然不会影响其他网站账号,但你在该网站的密码肯定要修改吧?但你没有一个很有效的方案修改密码,因为该密码是根据你的初始密钥计算出来的,如果要修改就要修改初始密钥,那就得修改所有网站的密码……
没办法,一个方案有它好的方面,必然有它不好的方面。所以我本人的解决方案是,用2种密码管理器,一种是本地型的,一种是本篇文章写的。我对于一些很重要的网站,比如支付宝、百度、Google、Youtube、Twitter、Freebuf、Github…这些网站均保存到本地密码管理器中,而在网络中其他乱七八糟的网站、或者是各种只有注册才能看的网站,都用本篇文章的密码管理器生成密码。对于重点网站重点保护,对于不重要的网站用普通的方式保存(但安全性也蛮高的)。
根据开发者文档的描述,用chrome.storage保存密码的方式貌似并不安全,欢迎大家提供更好的方式给我~
最后,我把该扩展的所有代码,打包上传到Github上了,欢迎大家提交issues和pull request哦!
NoSync-PasswordManager : https://github.com/mscb402/NoSync-PasswordManager
*本文原创作者:mscb,本文属FreeBuf原创奖励计划,未经许可禁止转载