#4 谷歌身份认证器

2021-03-08

基础概念

身份验证器

Authenticator

密码就算是身份验证器。
加密密钥,比如 SSH 的公钥私钥(密钥对)

动态密码

One-time Password, 缩写: OTP, 又叫做一次性密码

我们常用的的短信验证码就是一种非常方便快捷的动态密码形式。
早些年一些网络服务,比如谷歌、网易通行证等,可能会提供一个动态口令表,包含十几个密码,可以保存为图片,或者纯文本,上面的密码可以逐个使用,每个密码只能用一次。

  • RFC4226 HOTP: An HMAC-Based One-Time Password Algorithm
  • RFC6238 TOTP: Time-Based One-Time Password Algorithm

HOTP 基于 HMAC 算法

简单来说就是根据密钥和计数器来生成一个一次性密码。
除了记住密钥之外,你还要记住这是第几次使用。

  • HOTP value = HOTP(K, C) mod 10d
  • HOTP(K, C) = truncate(HMACH(K, C))
  • truncate(MAC) = extract31(MAC, MAC[(19 × 8) + 4:(19 × 8) + 7] × 8)
  • MAC[(19 × 8) + 4:(19 × 8) + 7] × 8
    取第十九字节的后四位(小端序), 转成一个有符号整型数(小端序),作为截取 MAC 的位置
  • extract31(MAC, i) = MAC[i × 8 + 1:i × 8 + (4 × 8) − 1]
  • MAC[i × 8 + 1:i × 8 + (4 × 8) − 1]
    从上一步得到的位置处,取四个字节,去掉符号位(小端序)

TOTP 基于时间

  1. 协商起始时间 T0 和时间间隔 TX
  2. 双方分别计算时间计数器 CT
  3. TOTP value(K) = HOTP value(K, CT)

多重要素验证

Multi-factor authentication, 缩写: MFA
Two-factor authentication, 缩写: 2FA

谷歌身份验证器

Google 身份验证器是一款 TOTP 与 HOTP 的两步验证软件令牌,此软件用于 Google 的认证服务。此项服务所使用的算法已列于 RFC 6238 和 RFC 4226 中。
Google 身份验证器给予用户一个六位到八位的一次性密码用于进行登录 Google 或其他站点时的附加验证。其同样可以给第三方应用生成口令,例如密码管理员或网络硬盘。先前版本的 Google 身份验证器开放源代码,但之后的版本以专有软件的形式公开。

谷歌验证器基于 TOTP,但是更进一步简化,以约定代替了协商过程。

  1. T0 为 Unix 时间
  2. TX 为 30 秒
  3. 哈希算法为 sha1

此外:

虽然不是很大的创新,而且这个软件验证器实现很简单,但是免费、开放(不需要做任何谷歌服务绑定),加上谷歌的强大影响力,这个软件验证器被很多系统采用。
最后,其他提供动态口令的应用都需要来兼容谷歌身份验证器。

PS: RedHat 开发并维护了开源的 FreeOTP 分支项目。

PS: 微软也有一个 Microsoft Authenticator,阿里云 APP 中有一个 虚拟MFA 功能,都是一个意思。
微软家的为自己提供 8 位密码,别人家的就 6 位,区别对待(虽然感觉好像也并没有什么影响)
阿里云 MFA 是需要手机 APP 登录进去之后才能使用的。

Just4Fun

import base64
import hashlib
import hmac
import time

DEFAULT_INTERVAL = 30  # Google Authenticator: 30 秒
DEFAULT_HASH = hashlib.sha1


def get_hotp_token(secret: str, counter: int, hash_algorithm=DEFAULT_HASH, length=6):
    padding_len = 8 - len(secret) % 8
    if padding_len != 8:
        assert 1 <= padding_len <= 7
        secret += '=' * padding_len
    key = base64.b32decode(secret, True)
    message = (counter & 0xffffffffffffffff).to_bytes(8, 'big')
    mac = hmac.new(key, message, hash_algorithm).digest()
    loc = mac[-1] & 0x0F
    token = (int.from_bytes(mac[loc:loc+4], 'big') & 0x7fffffff) % (10 ** length)
    return token


def get_totp_token(secret, interval=DEFAULT_INTERVAL):
    time_counter = int(time.time()) // interval
    return get_hotp_token(secret, time_counter)


if __name__ == '__main__':
    print('%06d' % get_totp_token(sys.argv[1]))

参考资料与拓展阅读

#3 SSL/TLS 相关信息

2020-03-13

名称

TLS, Transport Layer Security, 传输层安全性协议
SSL, Secure Sockets Layer, 安全套接层

历史

  1. 90 年代,WWW 先驱网景公司开发 SSL,用于提升 Web 安全性。
  2. 1996,SSL 开始由 IETF (The Internet Engineering Task Force, 互联网工程任务组) 标准化,最后在 1999 年成为 RFC 2246,名字改成了 TLS。
  3. TLS 1.0 约等于 SSL 3.0
  4. 微软 IE 也支持 TLS 1.0
  5. 现在,SSL 时期的三个版本,均已被彻底废弃。
  6. 由于历史原因,很多场合如果不严格区分版本,SSL 等于 TLS。
  7. TLS 1.2 在 2008 年成为 IETF 推荐的版本(2018 年被 TLS 1.3 淘汰)
  8. TLS 1.3 于 2018 年 8 月发表,它的突破性改进包括握手更快从而加快连接速度、简化支持的加密方式、速度和性能优于 TLS 1.2。
协议 发布时间 状态 说明
SSL 1.0 未公布 未公布  
SSL 2.0 1995 年 2011 年弃用  
SSL 3.0 1996 年 2015 年弃用  
TLS 1.0 1999 年 2021 年弃用 RFC 2246
TLS 1.1 2006 年 2021 年弃用 RFC 4346
TLS 1.2 2008 年   RFC 5246
TLS 1.3 2018 年   RFC 8446

旧版本的废弃

  1. SSL 1.0 从未发布。
  2. 2011 年 3 月,RFC 6176 删除了对 SSL 的兼容,避免通过协商使用已经被废弃的 SSL 2.0 而出现安全问题。
  3. 2014 年 10 月,Google 发现 SSL 3.0 有设计缺陷,可以将 TLS 安全连接强行降级到过时且不安全的 SSL 3.0。之后,Google 在自己公司相关产品中陆续禁止回溯兼容,强制使用 TLS 协议。
  4. 2015 年,正式废弃 SSL 3.0。
  5. 微软、Google、苹果、Mozilla 四家浏览器厂商在 2020 年终止支持 TLS 1.0 及 1.1。
  6. 2021 年 3 月,RFC 8996 标准弃用了 TLS 1.0 和 TLS 1.1。

现在主流的是 TLS 1.2 和 TLS 1.3。

相比之下,TLS 1.3 安全性更好,性能也更好。
搜索一下 tls1.2 tls1.3 difference 或者 tls1.2 tls1.3 performance 就能看到很多相关比较。

作用

SSL/TLS 的本质就是非对称加密。

细节

安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。
该协议由两层组成:

  • TLS 记录协议(TLS Record)
  • TLS 握手协议(TLS Handshake)

握手协议(handshake protocol)
密钥规格变更协议(change cipher spec protocol)
应用数据协议(application data protocol)
警报协议(alert protocol)。

参考资料与拓展阅读

#2 Djanog 密码算法:PBKDF2

2015-04-06

实现

在 Python 2.7.8 (July 2, 2014),或 Python 3.4 (March 17, 2014) 以上版本,标准库 hashlib 有 pbkdf2_hmac:

import base64
import hashlib
import random
import secrets

def pbkdf2(password, salt, iterations, dklen=0, digest=None):
    if digest is None:
        digest = hashlib.sha256
    if not dklen:
        dklen = None
    password = password.encode('utf-8')
    salt = salt.encode('utf-8')
    return hashlib.pbkdf2_hmac(
        digest().name, password, salt, iterations, dklen)

### pbkdf2_sha256 ###

ALGORITHM = 'pbkdf2_sha256'
ITERATIONS = 30000
DIGEST = hashlib.sha256

def encode(password, salt, iterations=None):
    assert password is not None
    assert salt and '$' not in salt
    if not iterations:
        iterations = ITERATIONS
    hash = pbkdf2(password, salt, iterations, digest=DIGEST)
    hash = base64.b64encode(hash).decode('ascii').strip()
    return "%s$%d$%s$%s" % (ALGORITHM, iterations, salt, hash)

def decode(encoded):
    algorithm, iterations, salt, hash = encoded.split('$', 3)
    assert algorithm == ALGORITHM
    return {
        'algorithm': algorithm,
        'hash': hash,
        'iterations': int(iterations),
        'salt': salt,
    }

def verify(password, encoded):
    algorithm, iterations, salt, hash = encoded.split('$', 3)
    assert algorithm == ALGORITHM
    encoded_2 = encode(password, salt, int(iterations))
    return secrets.compare_digest(encoded.encode('utf-8'),
                                  encoded_2.encode('utf-8'))

salt_length = 32
# salt = bytes([random.randint(0, 255) for i in range(salt_length + 20)])
salt = random.randbytes(salt_length + 20)
salt = salt.replace(b'$', b'').decode('latin')[:salt_length]
# print(repr(salt))
a = encode('123456', salt)
print(repr(a))
print(decode(a))
# 'pbkdf2_sha256$30000$x\x82¨\x07Ó[Ám¬9V¬\x13·sÉ\x1eEܱ3\x04Ü\x07\x05BÁfM\x9fV×$/jqvasjfZX6c9xCytBVqddAJFve2DXcLWClTAsZvl48='
print(verify('234567', a))
print(verify('123456', a))

参考资料与拓展阅读

#1 Django 密码

2015-02-05

https://docs.djangoproject.com/en/2.0/topics/auth/passwords/

在 Django 中,密码哈希存储在 auth_user 表中的 password 字段中。该字段的值包含了算法名称、迭代次数、盐值和哈希值,它们都是使用特定的格式进行编码的。例如,一个密码哈希的值可能如下所示:

pbkdf2_sha256$150000$V7v0fjbhMIhq$Gd/0XuOqK3ib6NBNGVwIKGXcFTUiNbTzdTNN8RiW24E=

用美元符号切割,得到 4 部分:

  • pbkdf2_sha256 算法名称
  • 150000 迭代次数
  • V7v0fjbhMIhq
  • Gd/0XuOqK3ib6NBNGVwIKGXcFTUiNbTzdTNN8RiW24E=

支持的哈希算法

PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.BCryptPasswordHasher',
]

默认使用的是 PBKDF2PasswordHasher

PBKDF2 是基于密钥的密码派生函数,它使用一个伪随机函数(PRF)和一个盐值来从给定密码派生出一个密钥。
PBKDF2 算法的核心是迭代的使用 PRF 函数,将每一次迭代的输出与前一次迭代的输出进行异或,生成最终的派生密钥。

大概逻辑如下:

import hashlib
import binascii
import os

class PBKDF2PasswordHasher:
    algorithm = 'pbkdf2_sha256'
    iterations = 100000

    def salt(self):
        return binascii.hexlify(os.urandom(16)).decode()

    def encode(self, password, salt):
        dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), self.iterations)
        return '%d$%s$%s' % (self.iterations, salt, binascii.hexlify(dk).decode())

    def verify(self, password, encoded):
        iterations, salt, dk = encoded.split('$')
        iterations = int(iterations)
        hashed_password = self.encode(password, salt)
        return hashed_password == encoded

生成密码

from django.contrib.auth.hashers import make_password

password = '123456'
hashed_password = make_password(password)

验证密码

from django.contrib.auth.hashers import check_password

is_correct_password = check_password(password, hashed_password)