文艺青年如何管理密码

这个问题的根源是,你不能所有的账户都用一样的密码,如果你胆敢那么干,并且你曾经注册过 CSDN 之类的网站,那么黑客拿到你的用户名和密码后,会去所有可能的网站,碰碰运气。

负责任的说,中奖的机率要比彩票高。

你也不用担心黑客劳累过度、积劳成疾,这些操作都可以自动化,保质保量,准时准点。

(如果实在想不出激情四射的密码,起码换个理性低调的马甲)

所以我最近读了多篇文章,大概是普通青年想摆脱二逼的困境,向着文艺升华的节奏,讲怎么置办一系列比较安全的密码。

首先,所有密码都高度相似,这肯定是非常二逼的;如果所有密码完全不同,怎么记住又是个问题。

普通的解决方案,密码分为不同的安全级别,不同的安全级别采取不同的策略。不重要的密码,可以随便用身份证号,你朋友的母亲的隔壁邻居的小孩的那一条狗的生日什么的糊弄过去;重要的密码,要用 1Password、LastPass之类的工具生成,然后保存在云端(所以这个其实并不适合存储银行卡密码)。

这里的问题是,密码都存储在云端,可能需要联网才能使用;并且还要依赖于服务商的安全管理水平和职业操守,LastPass 经常爆安全漏洞,并且大面积丢失过密码(如果密码是随机生成的,丢失了后,恐怕也只能一边哼着“密码哪去了”,一边设法重置了)。

所以……我的意思不是说文艺青年应当用 1Password,它们本质上是一样的。

我们的目标是什么呢?

复杂

不重复

己知部分密码,不能轻易的推出其它密码

容易记住

其实 hash 加密很容易达成这样的目标,以 SHA1 为例:

do_you_know_my_passwd?1 -> 740845e8ddf457a518a4c0ce3d44a95924e80497

do_you_know_my_passwd?2 -> b28ab233ba23dc844bf1095606f80962b244e019

它的特点有:

很长,上面结果是 16 进制数,转换成 10 进制还要更长一点

不可逆,知道的密码,不可能推断出原文——这点是算法本身保证的

对扰动敏感,上面两个例子只有一个字符不同,密码完全不一样。可以用主密码加描述的方式,比如 passwd#Kardinal@linuxtoy.org, 来生成密码

固定的输入对应固定的输出,这样只需要记主密码,而密码描述,完全可以打印出来随意张贴——只要主密码不同,生成的密码是完全不同的

相信很多文艺青年也想到了这种方式,但是为什么没有人采用呢?

废话,我也不这用这种方式的,因为:

长度固定,可能有的情况下密码只允许最多 n 位,当然你可以只取 n 位,这个问题到是不大

单调,没有文采,有的情况要求“大小写+数字+特殊字符”

现在我们已经从本质上解决了密码的问题,只需要一个这样的函数: gen_pwd primary_pwd desc rev :: str -> str -> int -> int

(三个参数为 主密码,描述,修订——有的时候你只是想单纯的更新一下密码,但是规则不变,怎么办呢?在待生成的字符串里加个修订版本就可以了。)

但是对于这个密码,还需要润色一下,让它能根据我们的要求变形,所以需要这样一个函数: fmt_pwd gen_pwd rule size retry :: int -> str -> int -> int -> str

(把刚才生成的密码,和规则、长度作为输入,输出美观的新密码。还有一个参数 retry 稍后再说)

先生成一个密码

  def gen_seed desc, rev
    (Digest::SHA1.hexdigest @primary + desc + rev.to_s).to_i(16)
  end

现在有了一个类似 b28ab233ba23dc844bf1095606f80962b244e019 这样的密码,假设需要 10 位,应该怎么办呢?

想到取前 10 位数的,拉出去毙了……

平均分成 10 等分,然后每4个数对应一位?

现在我需要一个 1000 位的密码,怎么分

假使真的把这个密码切成一段一段的,我可以保证,非常麻烦,是个体力活。

我觉得最方便的办法,是线性同余,这是一个生成伪随机数列的方法。感受下,伪~随机,“伪”表示它的输出是固定的,

如果每次是真的随机的话,你只能唱“密码去哪儿了……”,根本停不下来的节奏;“随机”意味着你生成的密码看起来就像掷骰子掷出来的一样。

  def lcg seed
    seed * 630360016 % 2147483647
  end

用这个方法生成指定长度的密码:

  def gen_pwd seed, size, try
    [*1..size].inject([seed + try]){|n, x| n << (lcg n[-1])}[1..size]
  end

假设需要 10 位密码, gen_pwd b28ab233ba23dc844bf1095606f80962b244e019 , 10 , 0 后,结果是这样的

[339466767, 236817397, 1269734706, 159396745, 121834885, 865587496, 446364382, 144844773, 1659694774, 1631169047]

参数里为什么要有个 try 呢? 线性同余虽然有优良的周期分布,但是不能保证任何情况下都符合要求——

生成一个只有特殊字符,而没有大小写和数字的密码,尽管概率极小,但不是完全没有可能。
这就需要对生成的密码进行校验,不符合要求的话,把 try 这个值递增上去重新生成,直到符合要求为止。

生成最终的密码:

  def fmt_pwd pwd, rule, custom
    rl = rule.flatten + custom.split('')
    pwd.map{|x| rl[x % rl.size] }
  end

校验是否符合要求:

  def verify pwd, rule
    return false unless pwd
    rst = rule.map do |r|
      lambda do
        for x in pwd
          return true if r.include? x
        end
        return false
      end[]
    end
    rst.inject{|n, x| n and x }
  end

最终的接口:

  def show desc, rule=false, rev:0, size:10, try:0, custom:@custom
    rl = unless rule then @@rule else rule.split('').map {|x| @@rules[x].to_a } end
    seed = gen_seed desc, rev
    result = nil
    until verify result, rl
      pwd = gen_pwd seed, size, try
      result = fmt_pwd pwd, rl, custom
      try += 1
    end
    PassWord.new result.join, try
  end

最终生成的密码差不多是这个样子的 :
"k1irM9wt0V", "l69WHePAj4", "lwVsF2TyLG"

最后,如果想用的舒服一点,可以自己设计一个用户级别的接口;另外可以把每条密码除主密码外的所有信息保存起来,JSON 或者数据库什么的;还可以在浏览器里把每条密码对应一个网址,实现自动填写……总之,你可以好好享受 DIY 的乐趣了

上面代码的

https://github.com/ran9er/diy_pwd

Read More: