PASETO 的 Swift 实现。
Paseto 拥有你所喜欢的 JOSE (JWT, JWE, JWS) 的一切优点,却没有任何 困扰 JOSE 标准的诸多设计缺陷。
Paseto (平台无关安全令牌) 是安全无状态令牌的规范。
与 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")
)
]
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
或 .v4
,purpose
是 .Public
(签名消息)或 .Local
(加密消息)。
由于 Version
和 Purpose
是枚举,建议你使用显式穷举(即没有默认值)的 switch-case 结构来选择不同的代码路径。 显式穷举可以确保,如果添加了额外的版本,Swift 编译器会通知你何时没有考虑所有可能性。
如果你尝试使用原始令牌创建消息,而该令牌生成的消息头与消息的类型参数不对应,则初始化程序将失败。
完全支持版本 4。
完全支持版本 3。
注意:对公共模式的支持需要
@available(macOS 11, iOS 14, watchOS 7, tvOS 14, macCatalyst 14, *)
。
完全支持版本 2。
版本 1(兼容性版本)由于兼容性问题(Swift 是一门新的语言 🤷♂️)仅部分支持 (讽刺的是)。
完全支持本地模式(即使用对称密钥加密 payload)下的版本 1。 目前不支持公共模式(即使用非对称密钥签名的 payload)下的版本 1。