Swift PASETO 构建状态

PASETO 的 Swift 实现。

Paseto 拥有你所喜欢的 JOSE (JWT, JWE, JWS) 的一切优点,却没有任何 困扰 JOSE 标准的诸多设计缺陷

目录

什么是 Paseto?

Paseto (平台无关安全令牌) 是安全无状态令牌的规范。

Paseto 和 JWT 的主要区别

与 JSON Web Tokens (JWT) 不同,JWT 为开发者提供了足够的绳索来自缢,Paseto 只允许安全的操作。 JWT 赋予你“算法灵活性”,Paseto 赋予你“版本控制协议”。 你几乎不可能以 不安全的方式 使用 Paseto。

注意: JWT 和 Paseto 都不是为 无状态会话管理 设计的。 Paseto 适用于防篡改 cookies,但本身不能防止重放攻击。

安装

使用 Swift Package Manager,将以下内容添加到你的 Package.swift 中。

dependencies: [
    .package(
        url: "https://github.com/aidantwoods/swift-paseto.git",
        .upToNextMajor(from: "1.0.0")
    )
]

Swift 库概述

Paseto Swift 库的设计目标是使用 Swift 编译器来捕获尽可能多的使用错误。

在某个时候,你作为用户必须决定使用哪个密钥来使用 Paseto。一旦你这样做,你实际上锁定了两件事:(i)你可以使用的 Paseto 令牌的版本,(ii)你要检查或生成的 payload 的类型(即,如果使用本地令牌则加密,如果使用公共令牌则签名)。

Paseto Swift 库通过类型参数(泛型)传递此信息,因此不可能出现整个类别的误用示例(例如,创建一个版本 2 密钥并意外地尝试生成版本 1 令牌,或者尝试解密签名令牌)。 事实上,甚至尝试这些示例的函数都不存在。

好的,那么这一切看起来像什么?

创建密钥时,只需将密钥类型名称附加到版本。 假设我们要生成一个新的版本 4 对称密钥

let symmetricKey = Version4.SymmetricKey()

好的,现在让我们创建一个令牌

var token = Token(claims: [
    "data":    "this is a signed message"
])

// set the expiry to 5 minutes from now
token.expiration = Date() + 5 * 60

现在加密它

guard let encrypted = try? token.encrypt(with: symmetricKey) else { /* respond to failure */ }

要解密令牌,我们需要解析它,并设置我们关心的任何验证规则

var parser = Parser<Version4.Local>()
guard let try? decryptedToken = parser.decrypt(encrypted, with: symmetricKey) else { /* respond to failure */ }

默认情况下,Parser 将使用 notExpired 检查进行初始化。 如果你在构造函数中设置自己的规则,则可以覆盖它。 如果你只想添加新规则,可以使用 addRule 方法而不删除此默认规则。

假设我们要生成一个新的版本 4 秘密(私钥)

let secretKey = Version4.AsymmetricSecretKey()

现在,如果我们希望生成一个可以被其他人验证的令牌,我们可以这样做

let publicKey = secretKey.publicKey // we need to save this so we can send it to others
guard let signed = try? token.sign(with: secretKey) else { /* respond to failure */ }

要验证用公钥 signed 的消息,例如 1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2

let pkHex = "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2"
guard let publicKey = try? Version4.AsymmetricPublicKey(hex: pkHex) else { /* this will fail if key is invalid */ }

var parser = Parser<Version4.Public>()
guard let try? verifiedToken = parser.verify(signed, with: publicKey) else { /* respond to failure */ }

最后,假设我们没有任何对象。 我们如何从字符串或数据创建消息和密钥?

让我们使用 Paseto 测试向量中的示例

Paseto 令牌如下(作为字符串/数据)

v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9

给定的十六进制对称密钥为

1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2

要生成令牌,请使用以下命令

let rawToken = "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"

guard let key = try? Version4.AsymmetricPublicKey(
    hex: "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2"
) else {
    /* respond to failure */
}

var parser = Parser<Version4.Public>(rules: []) // setting rules to empty to remove expiry check:
                                                // this is only necessary for demonstration purposes because this token has expired
guard let token = try? parser.verify(rawToken, with: key) else {
    /* respond to failure */
}

// the following will succeed
assert(token.claims == ["data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"])
assert(token.footer == "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}")

也可以使用 url 安全的 base64(不带填充)通过 init(encoded: String) 或使用原始密钥材料作为数据通过 init(material: Data) 创建密钥。

如果你需要确定收到的原始令牌的类型,你可以使用辅助函数 Util.header(of: String) -> Header? 来检索对应于给定令牌的 Header。 这仅检查给定的字符串是否为有效格式,并不保证任何关于内容的信息。

例如,使用上面的 rawToken

guard let header = Util.header(of: rawToken) else { /* this isn't a valid Paseto token */ }

Header 的结构如下

struct Header {
    let version: Version
    let purpose: Purpose
}

其中 version.v1.v2.v3.v4purpose.Public(签名消息)或 .Local(加密消息)。

由于 VersionPurpose 是枚举,建议你使用显式穷举(即没有默认值)的 switch-case 结构来选择不同的代码路径。 显式穷举可以确保,如果添加了额外的版本,Swift 编译器会通知你何时没有考虑所有可能性。

如果你尝试使用原始令牌创建消息,而该令牌生成的消息头与消息的类型参数不对应,则初始化程序将失败。

支持的 Paseto 版本

版本 4

完全支持版本 4。

版本 3

完全支持版本 3。

注意:对公共模式的支持需要 @available(macOS 11, iOS 14, watchOS 7, tvOS 14, macCatalyst 14, *)

版本 2

完全支持版本 2。

版本 1 (部分)

版本 1(兼容性版本)由于兼容性问题(Swift 是一门新的语言 🤷‍♂️)仅部分支持 (讽刺的是)。

完全支持本地模式(即使用对称密钥加密 payload)下的版本 1。 目前不支持公共模式(即使用非对称密钥签名的 payload)下的版本 1。