Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscribers Model #4

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions Sources/WebPush/Subscriber.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
//
// Subscriber.swift
// swift-webpush
//
// Created by Dimitri Bouniol on 2024-12-10.
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
//

@preconcurrency import Crypto
import Foundation

/// Represents a subscriber registration from the browser.
///
/// Prefer to use ``Subscriber`` directly when possible.
///
/// - SeeAlso: [Push API Working Draft §8. `PushSubscription` interface](https://www.w3.org/TR/push-api/#pushsubscription-interface). Note that the VAPID Key ID must be manually added to the structure supplied by the spec.
public protocol SubscriberProtocol: Sendable {
/// The endpoint representing the subscriber on their push registration service of choice.
var endpoint: URL { get }

/// The key material supplied by the user agent.
var userAgentKeyMaterial: UserAgentKeyMaterial { get }

/// The preferred VAPID Key ID to use, if available.
///
/// If unknown, use the key set to ``VoluntaryApplicationServerIdentification/Configuration/primaryKey``, but be aware that this may be different from the key originally used at time of subscription, and if it is, push messages will be rejected.
///
/// - Important: It is highly recommended to store the VAPID Key ID used at time of registration with the subscriber, and always supply the key itself to the manager. If you are phasing out the key and don't want new subscribers registered against it, store the key in ``VoluntaryApplicationServerIdentification/Configuration/deprecatedKeys``, otherwise store it in ``VoluntaryApplicationServerIdentification/Configuration/keys``.
var vapidKeyID: VAPID.Key.ID { get }
}

/// The set of cryptographic secrets shared by the browser (is. user agent) along with a subscription.
///
/// - SeeAlso: [RFC8291 Message Encryption for Web Push §2.1. Key and Secret Distribution](https://datatracker.ietf.org/doc/html/rfc8291#section-2.1)
public struct UserAgentKeyMaterial: Sendable {
/// The underlying type of an authentication secret.
public typealias Salt = Data

/// The public key a shared secret can be derived from for message encryption.
///
/// - SeeAlso: [Push API Working Draft §8.1. `PushEncryptionKeyName` enumeration — `p256dh`](https://www.w3.org/TR/push-api/#dom-pushencryptionkeyname-p256dh)
public var publicKey: P256.Signing.PublicKey

/// The authentication secret to validate our ability to send a subscriber push messages.
///
/// - SeeAlso: [Push API Working Draft §8.1. `PushEncryptionKeyName` enumeration — `auth`](https://www.w3.org/TR/push-api/#dom-pushencryptionkeyname-auth)
public var authenticationSecret: Salt

/// Initialize key material with a public key and authentication secret from a user agent.
///
/// - Parameters:
/// - publicKey: The public key a shared secret can be derived from for message encryption.
/// - authenticationSecret: The authentication secret to validate our ability to send a subscriber push messages.
public init(
publicKey: P256.Signing.PublicKey,
authenticationSecret: Salt
) {
self.publicKey = publicKey
self.authenticationSecret = authenticationSecret
}

/// Initialize key material with a public key and authentication secret from a user agent.
///
/// - Parameters:
/// - publicKey: The public key a shared secret can be derived from for message encryption.
/// - authenticationSecret: The authentication secret to validate our ability to send a subscriber push messages.
public init(
publicKey: String,
authenticationSecret: String
) throws {
guard let publicKeyData = Data(base64URLEncoded: publicKey)
else { throw CancellationError() } // invalid public key error (underlying error = URLDecoding error)
do {
self.publicKey = try P256.Signing.PublicKey(x963Representation: publicKeyData)
} catch { throw CancellationError() } // invalid public key error (underlying error = error)

guard let authenticationSecretData = Data(base64URLEncoded: authenticationSecret)
else { throw CancellationError() } // invalid authentication secret error (underlying error = URLDecoding error)

self.authenticationSecret = authenticationSecretData
}
}

extension UserAgentKeyMaterial: Hashable {
public static func == (lhs: UserAgentKeyMaterial, rhs: UserAgentKeyMaterial) -> Bool {
lhs.publicKey.x963Representation == rhs.publicKey.x963Representation
&& lhs.authenticationSecret == rhs.authenticationSecret
}

public func hash(into hasher: inout Hasher) {
hasher.combine(publicKey.x963Representation)
hasher.combine(authenticationSecret)
}
}

extension UserAgentKeyMaterial: Codable {
/// The encoded representation of a subscriber's key material.
///
/// - SeeAlso: [Push API Working Draft §8.1. `PushEncryptionKeyName` enumeration](https://www.w3.org/TR/push-api/#pushencryptionkeyname-enumeration)
public enum CodingKeys: String, CodingKey {
case publicKey = "p256dh"
case authenticationSecret = "auth"
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let publicKeyString = try container.decode(String.self, forKey: .publicKey)
let authenticationSecretString = try container.decode(String.self, forKey: .authenticationSecret)
try self.init(publicKey: publicKeyString, authenticationSecret: authenticationSecretString)
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(publicKey.x963Representation.base64URLEncodedString(), forKey: .publicKey)
try container.encode(authenticationSecret.base64URLEncodedString(), forKey: .authenticationSecret)
}
}

/// A default subscriber implementation that can be used to decode subscriptions encoded by client-side JavaScript directly.
///
/// Note that this object requires the VAPID key (`applicationServerKey` in JavaScript) that was supplied during registration, which is not provided by default by [`PushSubscription.toJSON()`](https://www.w3.org/TR/push-api/#dom-pushsubscription-tojson):
/// ```js
/// const subscriptionStatusResponse = await fetch(`/registerSubscription`, {
/// method: "POST",
/// body: {
/// ...subscription.toJSON(),
/// applicationServerKey: subscription.options.applicationServerKey,
/// }
/// });
/// ```
///
/// If you cannot provide this for whatever reason, opt to decode the object using your own type, and conform to ``SubscriberProtocol`` instead.
public struct Subscriber: SubscriberProtocol, Codable, Hashable, Sendable {
/// The encoded representation of a subscriber.
///
/// - Note: The VAPID Key ID must be manually added to the structure supplied by the spec.
/// - SeeAlso: [Push API Working Draft §8. `PushSubscription` interface](https://www.w3.org/TR/push-api/#pushsubscription-interface).
public enum CodingKeys: String, CodingKey {
case endpoint = "endpoint"
case userAgentKeyMaterial = "keys"
case vapidKeyID = "applicationServerKey"
}

/// The push endpoint associated with the push subscription.
///
/// - SeeAlso: [Push API Working Draft §8. `PushSubscription` interface — `endpoint`](https://www.w3.org/TR/push-api/#dfn-getting-the-endpoint-attribute)
public var endpoint: URL

/// The key material provided by the user agent to encrupt push data with.
///
/// - SeeAlso: [Push API Working Draft §8. `PushSubscription` interface — `getKey`](https://www.w3.org/TR/push-api/#dom-pushsubscription-getkey)
public var userAgentKeyMaterial: UserAgentKeyMaterial

/// The VAPID Key ID used to register the subscription, that identifies the application server with the push service.
///
/// - SeeAlso: [Push API Working Draft §8. `PushSubscription` interface — `options`](https://www.w3.org/TR/push-api/#dom-pushsubscription-options)
public var vapidKeyID: VAPID.Key.ID

/// Initialize a new subscriber manually.
///
/// Prefer decoding a subscription directly with the results of the subscription directly:
/// ```js
/// const subscriptionStatusResponse = await fetch(`/registerSubscription`, {
/// method: "POST",
/// body: {
/// ...subscription.toJSON(),
/// applicationServerKey: subscription.options.applicationServerKey,
/// }
/// });
/// ```
public init(
endpoint: URL,
userAgentKeyMaterial: UserAgentKeyMaterial,
vapidKeyID: VAPID.Key.ID
) {
self.endpoint = endpoint
self.userAgentKeyMaterial = userAgentKeyMaterial
self.vapidKeyID = vapidKeyID
}
}

extension Subscriber: Identifiable {
public var id: String { endpoint.absoluteString }
}
8 changes: 8 additions & 0 deletions Sources/WebPush/VAPID/VAPIDKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import Foundation

extension VoluntaryApplicationServerIdentification {
/// Represents the application server's identification key that is used to confirm to a push service that the server connecting to it is the same one that was subscribed to.
///
/// When sharing with the browser, ``VoluntaryApplicationServerIdentification/Key/ID`` can be used.
public struct Key: Sendable {
private var privateKey: P256.Signing.PrivateKey

Expand Down Expand Up @@ -46,6 +49,11 @@ extension VAPID.Key: Codable {
}

extension VAPID.Key: Identifiable {
/// The identifier for a private ``VoluntaryApplicationServerIdentification/Key``'s public key.
///
/// This value can be shared as is with a subscription registration as the `applicationServerKey` key in JavaScript.
///
/// - SeeAlso: [Push API Working Draft §7.2. PushSubscriptionOptions Interface](https://www.w3.org/TR/push-api/#pushsubscriptionoptions-interface)
public struct ID: Hashable, Comparable, Codable, Sendable, CustomStringConvertible {
private var rawValue: String

Expand Down
Loading