Skip to content

Commit 02f27b6

Browse files
Added VAPID types
1 parent 63db911 commit 02f27b6

File tree

4 files changed

+263
-0
lines changed

4 files changed

+263
-0
lines changed

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ let package = Package(
1515
.library(name: "WebPush", targets: ["WebPush"]),
1616
],
1717
dependencies: [
18+
.package(url: "https://github.com/apple/swift-crypto.git", "3.10.0"..<"5.0.0"),
1819
],
1920
targets: [
2021
.target(
2122
name: "WebPush",
2223
dependencies: [
24+
.product(name: "Crypto", package: "swift-crypto"),
2325
]
2426
),
2527
.testTarget(name: "WebPushTests", dependencies: ["WebPush"]),

Sources/WebPush/VAPID/VAPID.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// VAPIDKey.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-03.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public typealias VAPID = VoluntaryApplicationServerIdentification
12+
13+
/// A set of types for Voluntary Application Server Identification, also known as VAPID.
14+
///
15+
/// - SeeAlso: [RFC8292](https://datatracker.ietf.org/doc/html/rfc8292)
16+
public enum VoluntaryApplicationServerIdentification: Sendable {}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//
2+
// VAPIDConfiguration.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-04.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
extension VoluntaryApplicationServerIdentification {
12+
public struct Configuration: Hashable, Codable, Sendable {
13+
/// The VAPID key that identifies the push service to subscribers.
14+
///
15+
/// This key should be shared by all instances of your push service, and should be kept secure. Rotating this key is not recommended as you'll lose access to subscribers that registered against it.
16+
///
17+
/// Some implementations will choose to use different keys per subscriber. In that case, choose to provide a set of keys instead.
18+
public var primaryKey: Key?
19+
public var keys: Set<Key>
20+
public var deprecatedKeys: Set<Key>?
21+
public var contactInformation: ContactInformation
22+
public var expirationDuration: Duration
23+
public var validityDuration: Duration
24+
25+
public init(
26+
key: Key,
27+
deprecatedKeys: Set<Key>? = nil,
28+
contactInformation: ContactInformation,
29+
expirationDuration: Duration = .hours(22),
30+
validityDuration: Duration = .hours(20)
31+
) {
32+
self.primaryKey = key
33+
self.keys = [key]
34+
var deprecatedKeys = deprecatedKeys ?? []
35+
deprecatedKeys.remove(key)
36+
self.deprecatedKeys = deprecatedKeys.isEmpty ? nil : deprecatedKeys
37+
self.contactInformation = contactInformation
38+
self.expirationDuration = expirationDuration
39+
self.validityDuration = validityDuration
40+
}
41+
42+
public init(
43+
primaryKey: Key?,
44+
keys: Set<Key>,
45+
deprecatedKeys: Set<Key>? = nil,
46+
contactInformation: ContactInformation,
47+
expirationDuration: Duration = .hours(22),
48+
validityDuration: Duration = .hours(20)
49+
) throws {
50+
self.primaryKey = primaryKey
51+
var keys = keys
52+
if let primaryKey {
53+
keys.insert(primaryKey)
54+
}
55+
guard !keys.isEmpty
56+
else { throw CancellationError() } // TODO: No keys error
57+
58+
self.keys = keys
59+
var deprecatedKeys = deprecatedKeys ?? []
60+
deprecatedKeys.subtract(keys)
61+
self.deprecatedKeys = deprecatedKeys.isEmpty ? nil : deprecatedKeys
62+
self.contactInformation = contactInformation
63+
self.expirationDuration = expirationDuration
64+
self.validityDuration = validityDuration
65+
}
66+
67+
public init(from decoder: any Decoder) throws {
68+
let container = try decoder.container(keyedBy: CodingKeys.self)
69+
70+
let primaryKey = try container.decodeIfPresent(Key.self, forKey: CodingKeys.primaryKey)
71+
let keys = try container.decode(Set<Key>.self, forKey: CodingKeys.keys)
72+
let deprecatedKeys = try container.decodeIfPresent(Set<Key>.self, forKey: CodingKeys.deprecatedKeys)
73+
let contactInformation = try container.decode(ContactInformation.self, forKey: CodingKeys.contactInformation)
74+
let expirationDuration = try container.decode(Duration.self, forKey: CodingKeys.expirationDuration)
75+
let validityDuration = try container.decode(Duration.self, forKey: CodingKeys.validityDuration)
76+
77+
try self.init(
78+
primaryKey: primaryKey,
79+
keys: keys,
80+
deprecatedKeys: deprecatedKeys,
81+
contactInformation: contactInformation,
82+
expirationDuration: expirationDuration,
83+
validityDuration: validityDuration
84+
)
85+
}
86+
}
87+
}
88+
89+
extension VAPID.Configuration {
90+
public enum ContactInformation: Hashable, Codable, Sendable {
91+
case url(URL)
92+
case email(String)
93+
94+
var urlString: String {
95+
switch self {
96+
case .url(let url): url.absoluteURL.absoluteString
97+
case .email(let email): "mailto:\(email)"
98+
}
99+
}
100+
101+
public init(from decoder: any Decoder) throws {
102+
let container = try decoder.singleValueContainer()
103+
let url = try container.decode(URL.self)
104+
105+
switch url.scheme?.lowercased() {
106+
case "mailto":
107+
let email = String(url.absoluteString.dropFirst("mailto:".count))
108+
if !email.isEmpty {
109+
self = .email(email)
110+
} else {
111+
throw DecodingError.typeMismatch(URL.self, .init(codingPath: decoder.codingPath, debugDescription: "Found a mailto URL with no email."))
112+
}
113+
case "http", "https":
114+
self = .url(url)
115+
default:
116+
throw DecodingError.typeMismatch(URL.self, .init(codingPath: decoder.codingPath, debugDescription: "Expected a mailto or http(s) URL, but found neither."))
117+
}
118+
}
119+
120+
public func encode(to encoder: any Encoder) throws {
121+
var container = encoder.singleValueContainer()
122+
try container.encode(urlString)
123+
}
124+
}
125+
126+
public struct Duration: Hashable, Comparable, Codable, ExpressibleByIntegerLiteral, AdditiveArithmetic, Sendable {
127+
public let seconds: Int
128+
129+
public init(seconds: Int) {
130+
self.seconds = seconds
131+
}
132+
133+
public static func < (lhs: Self, rhs: Self) -> Bool {
134+
lhs.seconds < rhs.seconds
135+
}
136+
137+
public init(from decoder: Decoder) throws {
138+
let container = try decoder.singleValueContainer()
139+
self.seconds = try container.decode(Int.self)
140+
}
141+
142+
public func encode(to encoder: any Encoder) throws {
143+
var container = encoder.singleValueContainer()
144+
try container.encode(self.seconds)
145+
}
146+
147+
public init(integerLiteral value: Int) {
148+
self.seconds = value
149+
}
150+
151+
public static func - (lhs: Self, rhs: Self) -> Self {
152+
Self(seconds: lhs.seconds - rhs.seconds)
153+
}
154+
155+
public static func + (lhs: Self, rhs: Self) -> Self {
156+
Self(seconds: lhs.seconds + rhs.seconds)
157+
}
158+
159+
public static func seconds(_ seconds: Int) -> Self {
160+
Self(seconds: seconds)
161+
}
162+
163+
public static func minutes(_ minutes: Int) -> Self {
164+
Self(seconds: minutes*60)
165+
}
166+
167+
public static func hours(_ hours: Int) -> Self {
168+
Self(seconds: hours*60*60)
169+
}
170+
}
171+
}

Sources/WebPush/VAPID/VAPIDKey.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// VAPIDKey.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-04.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
@preconcurrency import Crypto
10+
import Foundation
11+
12+
extension VoluntaryApplicationServerIdentification {
13+
public struct Key: Sendable {
14+
private var privateKey: P256.Signing.PrivateKey
15+
16+
public init() {
17+
privateKey = P256.Signing.PrivateKey(compactRepresentable: false)
18+
}
19+
20+
public init(privateKey: P256.Signing.PrivateKey) {
21+
self.privateKey = privateKey
22+
}
23+
}
24+
}
25+
26+
extension VAPID.Key: Hashable {
27+
public static func == (lhs: Self, rhs: Self) -> Bool {
28+
lhs.privateKey.rawRepresentation == rhs.privateKey.rawRepresentation
29+
}
30+
31+
public func hash(into hasher: inout Hasher) {
32+
hasher.combine(privateKey.rawRepresentation)
33+
}
34+
}
35+
36+
extension VAPID.Key: Codable {
37+
public init(from decoder: any Decoder) throws {
38+
let container = try decoder.singleValueContainer()
39+
privateKey = try P256.Signing.PrivateKey(rawRepresentation: container.decode(Data.self))
40+
}
41+
42+
public func encode(to encoder: any Encoder) throws {
43+
var container = encoder.singleValueContainer()
44+
try container.encode(privateKey.rawRepresentation)
45+
}
46+
}
47+
48+
extension VAPID.Key: Identifiable {
49+
public struct ID: Hashable, Comparable, Codable, Sendable {
50+
private var rawValue: String
51+
52+
init(_ rawValue: String) {
53+
self.rawValue = rawValue
54+
}
55+
56+
public static func < (lhs: Self, rhs: Self) -> Bool {
57+
lhs.rawValue < rhs.rawValue
58+
}
59+
60+
public init(from decoder: any Decoder) throws {
61+
let container = try decoder.singleValueContainer()
62+
self.rawValue = try container.decode(String.self)
63+
}
64+
65+
public func encode(to encoder: any Encoder) throws {
66+
var container = encoder.singleValueContainer()
67+
try container.encode(self.rawValue)
68+
}
69+
}
70+
71+
public var id: ID {
72+
ID(privateKey.publicKey.x963Representation.base64EncodedString()) // TODO: make url-safe
73+
}
74+
}

0 commit comments

Comments
 (0)