#8 端对端加密通信软件

2023-01-29

相关文章:

端对端加密(End-to-end encryption,缩写为 E2EE)指的是,只有对话双方能够看到通信内容,其他第三方都无法解密,属于最安全的通信方式。
注意:按照国内相关法律,提供、使用无法监管的加密通讯工具有法律风险。

目前,支持端对端加密的通信软件,主要有下面这些:

  • Signal,据说是最安全的通信工具
  • Wire
  • Riot(Matrix 协议的实现)
  • Line
  • iMessage
  • Keybase
  • WhatsApp
  • Telegram
    默认没有启用端对端加密,只有打开"秘密对话"功能才会启用。
  • Status 去中心化,Matrix 加密通信协议
  • CoyIM 洋葱网络项目开发的匿名通信工具,Tor + TLS + XMPP + OTR(Off-The-Record)端对端加密

参考资料与拓展阅读

#7 scrypt 加密

2021-10-03

在 Python 标准库 hashlib 中见过 scrypt,说是 3.6 引入。
然后,这两天看到的资讯说是 Django 4 将加入了 scrypt 做密码哈希,据说安全性比之前的 PBKDF2 更好。
PS: 由于需要 OpenSSL 1.1+ 的支持,以及会消耗更多的内存,所以不是默认选项。

#5 JWT 认证

2021-09-20

JWT 介绍

一句话介绍 JWT:就是通过非对称加密算法签名的方式,将部分用户信息存在客户端。

全名 JSON Web Token,简称 JWT。
内容 = 头 Headers (JSON) + 负载 Payload (JSON) + 签名 Signature
分别 Base64 编码之后,用 . 连接。

示例

JSON 实现

import base64

def jsonify(obj):
    return json.dumps(obj, ensure_ascii=False, separators=',:')

def base64urlEncoding(s):
    return base64.urlsafe_b64encode(s).rstrip(b'=').decode('ascii')

def sign(key, msg):
    return base64urlEncoding(hmac.new(key, msg, hashlib.sha256).digest())

# def verify(key, msg, sig):
#     return key.verify(msg, base64.urlsafe_b64decode(sig))

def e(obj):
    return base64urlEncoding(jsonify(header).encode('utf-8'))

def encode(key, header, payload):
    s = e(header) + '.' + e(payload)
    return s + '.' + sign(key, s)

def decode(token):
    header_str, payload_str, sig = token.split('.')
    header = json.loads(base64.urlsafe_b64decode(header_str).decode('utf-8'))
    payload = json.loads(base64.urlsafe_b64decode(payload_str).decode('utf-8'))
    sig = base64.urlsafe_b64decode(sig)

header = {
  "alg": "HS256",
  "typ": "JWT"
}
payload = {
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

signature = HMAC_SHA256(
    secret,
    base64urlEncoding(header) + '.' +
    base64urlEncoding(payload)
)
const token = base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)

标准字段

Header 字段

Code Name Description
typ Type Token 类型,通常为 JWT,用于声明整个对象的类型。
cty Content Type 内容类型,用于声明 Payload 中嵌套对象的类型;嵌套 JWT 时常使用 JWT。
alg Algorithm 签名或加密所使用的算法,例如 HS256、RS256、ES256。
kid Key ID 密钥标识,用于帮助接收方选择对应的验证密钥。
x5c X.509 Certificate Chain X.509 证书链,以 Base64 编码的 DER 证书数组表示。
x5u X.509 URL 指向 X.509 公钥证书或证书链的 URL。
x5t X.509 SHA-1 Thumbprint X.509 证书的 SHA-1 指纹。
x5t#S256 X.509 SHA-256 Thumbprint X.509 证书的 SHA-256 指纹。
jku JWK Set URL 指向 JSON Web Key Set(JWKS)的 URL,用于获取验证所需的公钥。
jwk JSON Web Key 直接在 Header 中携带用于验证签名的 JSON Web Key。

Claim 字段(Payload 字段)

Code Name Description
iss Issuer Token 的签发者。
sub Subject Token 所代表的主体,通常用于表示用户或资源的唯一标识。
aud Audience Token 的受众,即允许使用该 Token 的服务或服务集合。
exp Expiration Time Token 的过期时间,使用 Unix 时间戳。
nbf Not Before Token 开始生效的时间,使用 Unix 时间戳。
iat Issued At Token 的签发时间,使用 Unix 时间戳。
jti JWT ID JWT 的唯一标识,可用于防重放、Token 撤销和审计追踪。

关于 JWT 的看法

  1. 相比传统 Session,JWT 可以减少认证状态存储带来的 DB/Redis 查询。

    • Session 通常需要服务端维护 Session 状态,请求到达后需要查询 Session Store。
    • JWT 将部分认证信息放在 Token 中,服务端验证签名即可完成认证,不需要每次查询 Session Store。
    • 但 JWT 并不意味着完全不需要 DB/Redis。业务数据、权限数据、Token 撤销状态等仍然可能需要查询。
  2. JWT 的主要优势是无状态认证,但“注销 / 踢下线”会重新引入状态。

    • JWT 一旦签发,在 exp 到期之前通常都有效。
    • 如果需要立即注销某个 Token,可以维护黑名单,例如:

      revoked_jti → expiration_time
      
    • 网关或认证服务可以定期同步黑名单,也可以通过 Redis 等共享存储实时查询。

    • 如果对“立即注销”的一致性要求不高,可以接受一定同步延迟。
    • 如果认证系统发生故障,可以根据业务要求选择继续验证 JWT或拒绝认证,但不能简单地认为“去掉黑名单检查就是降级”;这实际上是安全策略的改变。
  3. 相比加密 Cookie,JWT 不依赖 HTTP Cookie,适用场景更加广泛。

    • Cookie 是 HTTP 状态管理机制,浏览器会自动携带。
    • JWT 本质上只是一个 Token,可以放在:

      • Authorization: Bearer <token>
      • Cookie
      • WebSocket 握手
      • 移动端请求
      • 服务间调用
    • 因此 JWT 与 Cookie 实际上不是完全互斥的概念:JWT 也可以放在 Cookie 中。

  4. JWT 应尽可能保持短小。

    • JWT 会随着请求发送,因此 Payload 越大,请求开销越大。
    • 通常只需要保存少量认证相关信息,例如:

      {
          "sub": "123456",
          "iss": "auth.example.com",
          "iat": 1780000000,
          "exp": 1780003600
      }
      
    • 不建议把用户资料、权限列表、大量业务数据直接塞进 JWT。

    • JWT 更适合表达“这个 Token 是谁签发的、代表谁、给谁使用、什么时候有效”,而不是作为一个用户数据容器。
    • 如果需要携带大量信息,应考虑通过 sub 等标识再查询业务数据。
  5. JWT 很适合在 API Gateway / BFF 层统一完成认证校验。

    • 网关可以统一完成:

      • Token 格式检查
      • 签名验证
      • exp / nbf 校验
      • iss / aud 校验
      • 黑名单检查
    • 校验通过后,可以将用户身份信息传递给后端服务,例如:

      X-User-ID: 123456
      X-User-Roles: admin
      
    • 这样业务服务无需重复实现 JWT 验证逻辑。

    • 但认证和授权不是一回事。网关完成身份认证后,具体业务服务仍然应该根据业务资源执行授权判断。
  6. JWT 对跨域 API 场景比较友好,但它本身并没有“解决跨域”。

    • 跨域主要由浏览器的 CORS 机制决定。
    • 如果使用:

      Authorization: Bearer <JWT>
      

      前端可以主动携带 Token,并通过服务端 CORS 配置允许 Authorization 请求头。

    • 相比依赖 Cookie 的认证方式,Bearer Token 不需要依赖浏览器的 Cookie 自动携带机制,因此在前后端分离、移动端、第三方 API 等场景下通常更加灵活。

    • 如果 JWT 放在 Cookie 中,那么仍然需要处理 SameSite、Secure、HttpOnly、CORS 等 Cookie 相关问题。
  7. JWT 最大的价值不是“比 Session 更高级”,而是把认证状态的一部分从服务端 Session Store 转移到了客户端 Token,从而实现无状态认证。

    代价也很明确:

    方案 服务端状态 服务端注销 请求大小 横向扩展
    Session 有 简单 小 依赖共享 Session
    JWT 无 不支持 较大 简单
    JWT + 黑名单 少量 简单 较大 需要共享黑名单

    传统单体 Web 应用使用 Session 更简单,多服务、API、网关认证等场景适合 JWT。

#4 PyCryptodome

2020-02-05

和 PyCrypto 的关系

PyCrypto 是 Python 界最知名的加密模块,它提供了一系列的加密算法,包括对称加密、非对称加密、哈希算法、签名算法等。
不过有一个很大的问题:上一个版本 2.6.1 发布于 2013-10-18,已经很多年没有维护了。

PyCryptodome 是 PyCrypto 的分叉,该项目在统一套代码的基础上提供了两种包:pycryptodome 和 pycryptodomex:

  1. 前者保持对 PyCrypto 的兼容,所有的代码都在 Crypto 名称下,
  2. 后者丢掉了历史包袱,放弃对 PyCrypto 的兼容,所有代码都在 Cryptodome 名称下。

  3. https://pypi.org/project/pycryptodome/

  4. https://pypi.org/project/pycryptodomex/
  5. https://www.pycryptodome.org/en/latest/
  6. https://github.com/Legrandin/pycryptodome/

其他加密模块

示例


#3 Python 加密解密

2017-05-04

#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))

参考资料与拓展阅读