66// Copyright © 2024 Mochi Development, Inc. All rights reserved.
77//
88
9+ @preconcurrency import Crypto
910import Foundation
1011
1112/// Represents a subscriber registration from the browser.
@@ -31,37 +32,89 @@ public protocol SubscriberProtocol: Sendable {
3132/// The set of cryptographic secrets shared by the browser (is. user agent) along with a subscription.
3233///
3334/// - SeeAlso: [RFC8291 Message Encryption for Web Push §2.1. Key and Secret Distribution](https://datatracker.ietf.org/doc/html/rfc8291#section-2.1)
34- public struct UserAgentKeyMaterial : Codable , Hashable , Sendable {
35- /// The encoded representation of a subscriber's key material.
36- ///
37- /// - SeeAlso: [Push API Working Draft §8.1. `PushEncryptionKeyName` enumeration](https://www.w3.org/TR/push-api/#pushencryptionkeyname-enumeration)
38- enum CodingKeys : String , CodingKey {
39- case publicKey = " p256dh "
40- case authenticationSecret = " auth "
41- }
35+ public struct UserAgentKeyMaterial : Sendable {
36+ /// The underlying type of an authentication secret.
37+ public typealias Salt = Data
4238
4339 /// The public key a shared secret can be derived from for message encryption.
4440 ///
4541 /// - SeeAlso: [Push API Working Draft §8.1. `PushEncryptionKeyName` enumeration — `p256dh`](https://www.w3.org/TR/push-api/#dom-pushencryptionkeyname-p256dh)
46- public var publicKey : String
42+ public var publicKey : P256 . Signing . PublicKey
4743
4844 /// The authentication secret to validate our ability to send a subscriber push messages.
4945 ///
5046 /// - SeeAlso: [Push API Working Draft §8.1. `PushEncryptionKeyName` enumeration — `auth`](https://www.w3.org/TR/push-api/#dom-pushencryptionkeyname-auth)
51- public var authenticationSecret : String
47+ public var authenticationSecret : Salt
5248
5349 /// Initialize key material with a public key and authentication secret from a user agent.
5450 ///
5551 /// - Parameters:
5652 /// - publicKey: The public key a shared secret can be derived from for message encryption.
5753 /// - authenticationSecret: The authentication secret to validate our ability to send a subscriber push messages.
5854 public init (
59- publicKey: String ,
60- authenticationSecret: String
55+ publicKey: P256 . Signing . PublicKey ,
56+ authenticationSecret: Salt
6157 ) {
6258 self . publicKey = publicKey
6359 self . authenticationSecret = authenticationSecret
6460 }
61+
62+ /// Initialize key material with a public key and authentication secret from a user agent.
63+ ///
64+ /// - Parameters:
65+ /// - publicKey: The public key a shared secret can be derived from for message encryption.
66+ /// - authenticationSecret: The authentication secret to validate our ability to send a subscriber push messages.
67+ public init (
68+ publicKey: String ,
69+ authenticationSecret: String
70+ ) throws {
71+ guard let publicKeyData = Data ( base64URLEncoded: publicKey)
72+ else { throw CancellationError ( ) } // invalid public key error (underlying error = URLDecoding error)
73+ do {
74+ self . publicKey = try P256 . Signing. PublicKey ( x963Representation: publicKeyData)
75+ } catch { throw CancellationError ( ) } // invalid public key error (underlying error = error)
76+
77+ guard let authenticationSecretData = Data ( base64URLEncoded: authenticationSecret)
78+ else { throw CancellationError ( ) } // invalid authentication secret error (underlying error = URLDecoding error)
79+
80+ self . authenticationSecret = authenticationSecretData
81+ }
82+ }
83+
84+ extension UserAgentKeyMaterial : Hashable {
85+ public static func == ( lhs: UserAgentKeyMaterial , rhs: UserAgentKeyMaterial ) -> Bool {
86+ lhs. publicKey. x963Representation == rhs. publicKey. x963Representation
87+ && lhs. authenticationSecret == rhs. authenticationSecret
88+ }
89+
90+ public func hash( into hasher: inout Hasher ) {
91+ hasher. combine ( publicKey. x963Representation)
92+ hasher. combine ( authenticationSecret)
93+ }
94+ }
95+
96+ extension UserAgentKeyMaterial : Codable {
97+ /// The encoded representation of a subscriber's key material.
98+ ///
99+ /// - SeeAlso: [Push API Working Draft §8.1. `PushEncryptionKeyName` enumeration](https://www.w3.org/TR/push-api/#pushencryptionkeyname-enumeration)
100+ public enum CodingKeys : String , CodingKey {
101+ case publicKey = " p256dh "
102+ case authenticationSecret = " auth "
103+ }
104+
105+ public init ( from decoder: Decoder ) throws {
106+ let container = try decoder. container ( keyedBy: CodingKeys . self)
107+
108+ let publicKeyString = try container. decode ( String . self, forKey: . publicKey)
109+ let authenticationSecretString = try container. decode ( String . self, forKey: . authenticationSecret)
110+ try self . init ( publicKey: publicKeyString, authenticationSecret: authenticationSecretString)
111+ }
112+
113+ public func encode( to encoder: any Encoder ) throws {
114+ var container = encoder. container ( keyedBy: CodingKeys . self)
115+ try container. encode ( publicKey. x963Representation. base64URLEncodedString ( ) , forKey: . publicKey)
116+ try container. encode ( authenticationSecret. base64URLEncodedString ( ) , forKey: . authenticationSecret)
117+ }
65118}
66119
67120/// A default subscriber implementation that can be used to decode subscriptions encoded by client-side JavaScript directly.
@@ -83,7 +136,7 @@ public struct Subscriber: SubscriberProtocol, Codable, Hashable, Sendable {
83136 ///
84137 /// - Note: The VAPID Key ID must be manually added to the structure supplied by the spec.
85138 /// - SeeAlso: [Push API Working Draft §8. `PushSubscription` interface](https://www.w3.org/TR/push-api/#pushsubscription-interface).
86- enum CodingKeys : String , CodingKey {
139+ public enum CodingKeys : String , CodingKey {
87140 case endpoint = " endpoint "
88141 case userAgentKeyMaterial = " keys "
89142 case vapidKeyID = " applicationServerKey "
0 commit comments