Skip to content

Commit 8a3e95b

Browse files
Added vapid-key generator utility to generate keys with
1 parent 492ea0a commit 8a3e95b

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ DerivedData/
77
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
88
.netrc
99
Package.resolved
10+
11+
/vapid-key-generator/.build
12+
/vapid-key-generator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,68 @@ targets: [
5151

5252
## Usage
5353

54+
### Generating Keys
55+
56+
Before integrating WebPush into your server, you must generate one time VAPID keys to identify your server to push services with. To help we this, we provide `vapid-key-generator`, which you can install and use as needed:
57+
```zsh
58+
% git clone https://github.com/mochidev/swift-webpush.git
59+
% cd swift-webpush/vapid-key-generator
60+
% swift package experimental-install
61+
```
62+
63+
To uninstall the generator:
64+
```zsh
65+
% package experimental-uninstall vapid-key-generator
66+
```
67+
68+
Once installed, a new configuration can be generated as needed:
69+
```
70+
% ~/.swiftpm/bin/vapid-key-generator https://example.com
71+
VAPID.Configuration: {"contactInformation":"https://example.com","expirationDuration":79200,"keys":["g7PXKzeMR/B+ndQWa92Dl9u22CibXJnm6vN9L6Gri1E="],"primaryKey":"g7PXKzeMR/B+ndQWa92Dl9u22CibXJnm6vN9L6Gri1E=","validityDuration":72000}
72+
73+
74+
Example Usage:
75+
// TODO: Load this data from .env or from file system
76+
let configurationData = Data(#" {"contactInformation":"https://example.com","expirationDuration":79200,"keys":["g7PXKzeMR/B+ndQWa92Dl9u22CibXJnm6vN9L6Gri1E="],"primaryKey":"g7PXKzeMR/B+ndQWa92Dl9u22CibXJnm6vN9L6Gri1E=","validityDuration":72000} "#.utf8)
77+
let vapidConfiguration = try JSONDecoder().decode(VAPID.Configuration.self, from: configurationData)
78+
```
79+
80+
Once generated, the configuration JSON should be added to your deployment's `.env` and kept secure so it can be accessed at runtime by your application server, and _only_ by your application server. Make sure this key does not leak and is not stored alongside subscriber information.
81+
82+
> [!NOTE]
83+
> You can specify either a support URL or an email for administrators of push services to contact you with if problems are encountered, or you can generate keys only if you prefer to configure contact information at runtime:
84+
85+
```zsh
86+
% ~/.swiftpm/bin/vapid-key-generator -h
87+
OVERVIEW: Generate VAPID Keys.
88+
89+
Generates VAPID keys and configurations suitable for use on your server. Keys should generally only be generated once
90+
and kept secure.
91+
92+
USAGE: vapid-key-generator <support-url>
93+
vapid-key-generator --email <email>
94+
vapid-key-generator --key-only
95+
96+
ARGUMENTS:
97+
<support-url> The fully-qualified HTTPS support URL administrators of push services may contact you at:
98+
https://example.com/support
99+
100+
OPTIONS:
101+
-k, --key-only Only generate a VAPID key.
102+
-s, --silent Output raw JSON only so this tool can be piped with others in scripts.
103+
-p, --pretty Output JSON with spacing. Has no effect when generating keys only.
104+
--email <email> Parse the input as an email address.
105+
-h, --help Show help information.
106+
```
107+
108+
> [!IMPORTANT]
109+
> If you only need to change the contact information, you can do so in the JSON directly — a key should _not_ be regenerated when doing this as it will invalidate all existing subscriptions.
110+
111+
> [!TIP]
112+
> If you prefer, you can also generate keys in your own code by calling `VAPID.Key()`, but remember, the key should be persisted and re-used from that point forward!
113+
114+
### Setup
115+
54116
TBD
55117

56118
## Contributing

vapid-key-generator/Package.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let tool = Package(
7+
name: "vapid-key-generator",
8+
platforms: [
9+
.macOS(.v13),
10+
.iOS(.v15),
11+
.tvOS(.v15),
12+
.watchOS(.v8),
13+
],
14+
products: [
15+
.executable(name: "vapid-key-generator", targets: ["VAPIDKeyGenerator"]),
16+
],
17+
dependencies: [
18+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
19+
.package(path: "../"),
20+
],
21+
targets: [
22+
.executableTarget(
23+
name: "VAPIDKeyGenerator",
24+
dependencies: [
25+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
26+
.product(name: "WebPush", package: "swift-webpush"),
27+
]
28+
),
29+
]
30+
)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// VAPIDKeyGenerator.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-14.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import ArgumentParser
10+
import Foundation
11+
import WebPush
12+
13+
@main
14+
struct MyCoolerTool: ParsableCommand {
15+
static let configuration = CommandConfiguration(
16+
abstract: "Generate VAPID Keys.",
17+
usage: """
18+
vapid-key-generator <support-url>
19+
vapid-key-generator --email <email>
20+
vapid-key-generator --key-only
21+
""",
22+
discussion: """
23+
Generates VAPID keys and configurations suitable for use on your server. Keys should generally only be generated once and kept secure.
24+
"""
25+
)
26+
27+
@Flag(name: [.long, .customShort("k")], help: "Only generate a VAPID key.")
28+
var keyOnly = false
29+
30+
@Flag(name: [.long, .customShort("s")], help: "Output raw JSON only so this tool can be piped with others in scripts.")
31+
var silent = false
32+
33+
@Flag(name: [.long, .customShort("p")], help: "Output JSON with spacing. Has no effect when generating keys only.")
34+
var pretty = false
35+
36+
@Option(name: [.long], help: "Parse the input as an email address.")
37+
var email: String?
38+
39+
@Argument(help: "The fully-qualified HTTPS support URL administrators of push services may contact you at: https://example.com/support") var supportURL: URL?
40+
41+
mutating func run() throws {
42+
let key = VAPID.Key()
43+
let encoder = JSONEncoder()
44+
if pretty {
45+
encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys, .prettyPrinted]
46+
} else {
47+
encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys]
48+
}
49+
50+
if keyOnly, supportURL == nil, email == nil {
51+
let json = String(decoding: try encoder.encode(key), as: UTF8.self)
52+
53+
if silent {
54+
print("\(json)")
55+
} else {
56+
print("VAPID.Key (with quotes): \(json)\n\n")
57+
print("Example Usage:")
58+
print(" // TODO: Load this data from .env or from file system")
59+
print(" let keyData = Data(#\" \(json) \"#.utf8)")
60+
print(" let vapidKey = try JSONDecoder().decode(VAPID.Key.self, from: keyData)")
61+
}
62+
} else if !keyOnly {
63+
let contactInformation = if let supportURL, email == nil {
64+
VAPID.Configuration.ContactInformation.url(supportURL)
65+
} else if supportURL == nil, let email {
66+
VAPID.Configuration.ContactInformation.email(email)
67+
} else if supportURL != nil, email != nil {
68+
throw UnknownError(reason: "Only one of an email or a support-url may be specified.")
69+
} else {
70+
throw UnknownError(reason: "A valid support-url must be specified.")
71+
}
72+
if let supportURL {
73+
guard let scheme = supportURL.scheme?.lowercased(), scheme == "http" || scheme == "https"
74+
else { throw UnknownError(reason: "support-url must be an HTTP or HTTPS.") }
75+
}
76+
77+
let configuration = VAPID.Configuration(key: key, contactInformation: contactInformation)
78+
79+
let json = String(decoding: try encoder.encode(configuration), as: UTF8.self)
80+
81+
if silent {
82+
print("\(json)")
83+
} else {
84+
var exampleJSON = ""
85+
if pretty {
86+
print("VAPID.Configuration:\n\(json)\n\n")
87+
exampleJSON = json
88+
exampleJSON.replace("\n", with: "\n ")
89+
exampleJSON = "#\"\"\"\n \(exampleJSON)\n \"\"\"#"
90+
} else {
91+
print("VAPID.Configuration: \(json)\n\n")
92+
exampleJSON = "#\" \(json) \"#"
93+
}
94+
print("Example Usage:")
95+
print(" // TODO: Load this data from .env or from file system")
96+
print(" let configurationData = Data(\(exampleJSON).utf8)")
97+
print(" let vapidConfiguration = try JSONDecoder().decode(VAPID.Configuration.self, from: configurationData)")
98+
}
99+
} else {
100+
if email != nil {
101+
throw UnknownError(reason: "An email cannot be specified if only keys are being generated.")
102+
} else {
103+
throw UnknownError(reason: "A support-url cannot be specified if only keys are being generated.")
104+
}
105+
}
106+
}
107+
}
108+
109+
struct UnknownError: LocalizedError {
110+
var reason: String
111+
112+
var errorDescription: String? { reason }
113+
}
114+
115+
extension URL: @retroactive ExpressibleByArgument {
116+
public init?(argument: String) {
117+
self.init(string: argument)
118+
}
119+
}

0 commit comments

Comments
 (0)