-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathVAPIDToken.swift
97 lines (79 loc) · 3.59 KB
/
VAPIDToken.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//
// VAPIDToken.swift
// swift-webpush
//
// Created by Dimitri Bouniol on 2024-12-07.
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
//
@preconcurrency import Crypto
import Foundation
extension VAPID {
/// An internal representation the token and authorization headers used self-identification.
///
/// - SeeAlso: [RFC8292 Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2)
struct Token: Hashable, Codable, Sendable {
enum CodingKeys: String, CodingKey {
case audience = "aud"
case subject = "sub"
case expiration = "exp"
}
var audience: String
var subject: VAPID.Configuration.ContactInformation
var expiration: Int
static let jwtHeader = Array(#"{"typ":"JWT","alg":"ES256"}"#.utf8).base64URLEncodedString()
init(
origin: String,
contactInformation: VAPID.Configuration.ContactInformation,
expiration: Date
) {
self.audience = origin
self.subject = contactInformation
self.expiration = Int(expiration.timeIntervalSince1970)
}
init(
origin: String,
contactInformation: VAPID.Configuration.ContactInformation,
expiresIn: VAPID.Configuration.Duration
) {
audience = origin
subject = contactInformation
expiration = Int(Date.now.timeIntervalSince1970) + expiresIn.seconds
}
init?(token: String, key: String) {
let components = token.split(separator: ".")
guard
components.count == 3,
components[0] == Self.jwtHeader,
let bodyBytes = Data(base64URLEncoded: components[1]),
let signatureBytes = Data(base64URLEncoded: components[2]),
let publicKeyBytes = Data(base64URLEncoded: key)
else { return nil }
let message = Data("\(components[0]).\(components[1])".utf8)
let publicKey = try? P256.Signing.PublicKey(x963Representation: publicKeyBytes)
let isValid = try? publicKey?.isValidSignature(.init(rawRepresentation: signatureBytes), for: SHA256.hash(data: message))
guard
isValid == true,
let token = try? JSONDecoder().decode(Self.self, from: bodyBytes)
else { return nil }
self = token
}
func generateJWT(signedBy signingKey: some VAPIDKeyProtocol) throws -> String {
let header = Self.jwtHeader
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
let body = try encoder.encode(self).base64URLEncodedString()
var message = "\(header).\(body)"
let signature = try message.withUTF8 { try signingKey.signature(for: $0) }.base64URLEncodedString()
return "\(message).\(signature)"
}
func generateAuthorization(signedBy signingKey: some VAPIDKeyProtocol) throws -> String {
let token = try generateJWT(signedBy: signingKey)
let key = signingKey.id
return "vapid t=\(token), k=\(key)"
}
}
}
protocol VAPIDKeyProtocol: Identifiable, Sendable {
associatedtype Signature: ContiguousBytes
func signature(for message: some DataProtocol) throws -> Signature
}