wkd-checker 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jonatan_M
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # WKD Checker
2
+ A Node.js library for retrieving and validating OpenPGP keys from Web Key Directory (WKD) servers.
3
+
4
+ ## Example Usage
5
+
6
+ ```js
7
+ const wkd = require('wkd-checker');
8
+
9
+ const email = 'test@example.com';
10
+
11
+ wkd.checkKey(email)
12
+ .then(result => {
13
+ console.log('WKD Result:', result);
14
+ console.log('Advanced:', result.advanced.valid);
15
+ console.log('Direct:', result.direct.valid);
16
+ })
17
+
18
+ wkd.getKey(email)
19
+ .then(key => {
20
+ console.log('Retrieved Key:\n', key);
21
+ })
22
+ .catch(err => {
23
+ console.error('Error retrieving key:', err);
24
+ });
25
+ ```
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Result of a key check operation.
3
+ */
4
+ export interface KeyCheckResult {
5
+ policyAvailable: boolean;
6
+ policyCorsValid: boolean;
7
+ key_location: string | null;
8
+ key_available: boolean;
9
+ keyCorsValid: boolean;
10
+ keyType: KeyType;
11
+ keyTypeValid: boolean;
12
+ fingerprint: string | null;
13
+ emailInKey: boolean;
14
+ valid: boolean;
15
+ }
16
+ /**
17
+ * Enum representing the type of key found.
18
+ */
19
+ export declare enum KeyType {
20
+ Invalid = "Invalid",
21
+ BinaryKey = "BinaryKey",
22
+ ArmoredKey = "ArmoredKey"
23
+ }
24
+ export declare class WKDError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ /**
28
+ * Validates an email address format.
29
+ * @param {string} email - The email address to validate.
30
+ * @returns {boolean} True if the email is valid, false otherwise.
31
+ */
32
+ export declare const validateEmail: (email: string) => boolean;
33
+ /**
34
+ * Search for a public key using Web Key Directory protocol.
35
+ * @param {string} email - User's email.
36
+ * @returns {Promise<Uint8Array>} The public key.
37
+ */
38
+ export declare const getKey: (email: string) => Promise<Uint8Array>;
39
+ /**
40
+ * Processes an email to find WKD keys using both Advanced and Direct methods.
41
+ * @param {string} email - The email address to look up.
42
+ * @returns {Promise<{ advanced: KeyCheckResult, direct: KeyCheckResult }>} Results for both lookup methods.
43
+ */
44
+ export declare const checkKey: (email: string) => Promise<{
45
+ advanced: KeyCheckResult;
46
+ direct: KeyCheckResult;
47
+ }>;
package/dist/index.js ADDED
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.checkKey = exports.getKey = exports.validateEmail = exports.WKDError = exports.KeyType = void 0;
13
+ const openpgp_1 = require("openpgp");
14
+ /**
15
+ * Enum representing the type of key found.
16
+ */
17
+ var KeyType;
18
+ (function (KeyType) {
19
+ KeyType["Invalid"] = "Invalid";
20
+ KeyType["BinaryKey"] = "BinaryKey";
21
+ KeyType["ArmoredKey"] = "ArmoredKey";
22
+ })(KeyType || (exports.KeyType = KeyType = {}));
23
+ class WKDError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "WKDError";
27
+ }
28
+ }
29
+ exports.WKDError = WKDError;
30
+ const EMAIL_REGEX = /^[^@]+@[^@]+\.[^@]+$/;
31
+ /**
32
+ * Validates an email address format.
33
+ * @param {string} email - The email address to validate.
34
+ * @returns {boolean} True if the email is valid, false otherwise.
35
+ */
36
+ const validateEmail = (email) => EMAIL_REGEX.test(email);
37
+ exports.validateEmail = validateEmail;
38
+ /**
39
+ * Fetches a URL and checks for CORS headers.
40
+ * @param {string} url - The URL to fetch.
41
+ * @param {RequestInit} options - Fetch options.
42
+ * @returns {Promise<{ response: Response | null, corsValid: boolean }>} The response and CORS validity status.
43
+ */
44
+ const fetchWithCorsCheck = (url, options) => __awaiter(void 0, void 0, void 0, function* () {
45
+ try {
46
+ const response = yield fetch(url, options);
47
+ const corsValid = response.headers.get("access-control-allow-origin") === "*";
48
+ return { response: response.ok ? response : null, corsValid };
49
+ }
50
+ catch (_a) {
51
+ return { response: null, corsValid: false };
52
+ }
53
+ });
54
+ /**
55
+ * Detects the type of OpenPGP key (Armor or Binary).
56
+ * @param {Response} keyData - The response containing the key data.
57
+ * @returns {Promise<{ keyType: KeyType, key: Key | null }>} The detected key type and parsed key object.
58
+ */
59
+ const detectKeyType = (keyData) => __awaiter(void 0, void 0, void 0, function* () {
60
+ try {
61
+ const buffer = yield keyData.clone().arrayBuffer();
62
+ const binaryKey = new Uint8Array(buffer);
63
+ const header = "-----BEGIN PGP PUBLIC KEY BLOCK-----";
64
+ const prefix = new TextDecoder().decode(binaryKey.slice(0, header.length));
65
+ if (prefix === header) {
66
+ const text = new TextDecoder().decode(binaryKey);
67
+ const key = yield (0, openpgp_1.readKey)({ armoredKey: text });
68
+ return { keyType: KeyType.ArmoredKey, key };
69
+ }
70
+ else {
71
+ const key = yield (0, openpgp_1.readKey)({ binaryKey });
72
+ return { keyType: KeyType.BinaryKey, key };
73
+ }
74
+ }
75
+ catch (_a) {
76
+ return { keyType: KeyType.Invalid, key: null };
77
+ }
78
+ });
79
+ /**
80
+ * Checks for a key at a specific WKD URL.
81
+ * @param {string} url - The WKD URL for the key.
82
+ * @param {string} policy - The policy URL.
83
+ * @param {RequestInit} options - Fetch options.
84
+ * @param {string} email - The email address used to verify the key user ID.
85
+ * @returns {Promise<KeyCheckResult>} The result of the key check.
86
+ */
87
+ const checkKeyUrl = (url, policy, options, email) => __awaiter(void 0, void 0, void 0, function* () {
88
+ var _a;
89
+ const [policyData, keyData] = yield Promise.all([
90
+ fetchWithCorsCheck(policy, options),
91
+ fetchWithCorsCheck(url, options)
92
+ ]);
93
+ const result = {
94
+ policyAvailable: !!policyData.response,
95
+ policyCorsValid: policyData.corsValid,
96
+ key_location: ((_a = keyData.response) === null || _a === void 0 ? void 0 : _a.url) || url,
97
+ key_available: !!keyData.response,
98
+ keyCorsValid: keyData.corsValid,
99
+ keyType: KeyType.Invalid,
100
+ keyTypeValid: false,
101
+ fingerprint: null,
102
+ emailInKey: false,
103
+ valid: false,
104
+ };
105
+ if (keyData.response) {
106
+ const { keyType, key } = yield detectKeyType(keyData.response);
107
+ result.keyType = keyType;
108
+ result.keyTypeValid = keyType === KeyType.BinaryKey;
109
+ if (key) {
110
+ const identities = yield key.getUserIDs();
111
+ result.emailInKey = identities.some(identity => identity.includes(email));
112
+ result.fingerprint = key.getFingerprint().toUpperCase();
113
+ }
114
+ }
115
+ result.valid = result.policyAvailable && result.policyCorsValid && result.key_available && result.keyCorsValid && result.keyTypeValid && result.emailInKey;
116
+ return result;
117
+ });
118
+ /**
119
+ * Encode input using Z-Base32 encoding.
120
+ *
121
+ * @param {Uint8Array} data - The binary data to encode
122
+ * @returns {String} Binary data encoded using Z-Base32.
123
+ */
124
+ function zbase32Encode(data) {
125
+ if (data.length === 0) {
126
+ return "";
127
+ }
128
+ const ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769";
129
+ const SHIFT = 5;
130
+ const MASK = 31;
131
+ let buffer = data[0];
132
+ let index = 1;
133
+ let bitsLeft = 8;
134
+ let result = '';
135
+ while (bitsLeft > 0 || index < data.length) {
136
+ if (bitsLeft < SHIFT) {
137
+ if (index < data.length) {
138
+ buffer <<= 8;
139
+ buffer |= data[index++] & 0xff;
140
+ bitsLeft += 8;
141
+ }
142
+ else {
143
+ const pad = SHIFT - bitsLeft;
144
+ buffer <<= pad;
145
+ bitsLeft += pad;
146
+ }
147
+ }
148
+ bitsLeft -= SHIFT;
149
+ result += ALPHABET[MASK & (buffer >> bitsLeft)];
150
+ }
151
+ return result;
152
+ }
153
+ /**
154
+ * Generates WKD URLs for a given email address.
155
+ * @param {string} email - The email address.
156
+ * @returns {Promise<{ advancedUrl: string, directUrl: string, advancedPolicyUrl: string, directPolicyUrl: string }>} The generated URLs.
157
+ */
158
+ const generateWkdUrls = (email) => __awaiter(void 0, void 0, void 0, function* () {
159
+ const [localPart, domain] = email.split('@');
160
+ // Spec Requirement: map uppercase ASCII to lowercase. Non-ASCII are not changed.
161
+ const localPartHashInput = localPart.replace(/[A-Z]/g, c => c.toLowerCase());
162
+ const domainLower = domain.toLowerCase();
163
+ const hashBuffer = yield crypto.subtle.digest("SHA-1", new TextEncoder().encode(localPartHashInput));
164
+ const hash = zbase32Encode(new Uint8Array(hashBuffer));
165
+ const nameParam = encodeURIComponent(localPart);
166
+ return {
167
+ advancedUrl: `https://openpgpkey.${domainLower}/.well-known/openpgpkey/${domainLower}/hu/${hash}?l=${nameParam}`,
168
+ directUrl: `https://${domainLower}/.well-known/openpgpkey/hu/${hash}?l=${nameParam}`,
169
+ advancedPolicyUrl: `https://openpgpkey.${domainLower}/.well-known/openpgpkey/policy`,
170
+ directPolicyUrl: `https://${domainLower}/.well-known/openpgpkey/policy`
171
+ };
172
+ });
173
+ /**
174
+ * Search for a public key using Web Key Directory protocol.
175
+ * @param {string} email - User's email.
176
+ * @returns {Promise<Uint8Array>} The public key.
177
+ */
178
+ const getKey = (email) => __awaiter(void 0, void 0, void 0, function* () {
179
+ if (!(0, exports.validateEmail)(email)) {
180
+ throw new WKDError('Invalid e-mail address.');
181
+ }
182
+ const { advancedUrl, directUrl } = yield generateWkdUrls(email);
183
+ const urls = [advancedUrl, directUrl];
184
+ for (const url of urls) {
185
+ try {
186
+ const response = yield fetch(url);
187
+ if (response.ok) {
188
+ return new Uint8Array(yield response.arrayBuffer());
189
+ }
190
+ }
191
+ catch (_a) {
192
+ // Ignore error and try next URL
193
+ }
194
+ }
195
+ throw new WKDError('No keys found');
196
+ });
197
+ exports.getKey = getKey;
198
+ /**
199
+ * Processes an email to find WKD keys using both Advanced and Direct methods.
200
+ * @param {string} email - The email address to look up.
201
+ * @returns {Promise<{ advanced: KeyCheckResult, direct: KeyCheckResult }>} Results for both lookup methods.
202
+ */
203
+ const checkKey = (email) => __awaiter(void 0, void 0, void 0, function* () {
204
+ const { advancedUrl, directUrl, advancedPolicyUrl, directPolicyUrl } = yield generateWkdUrls(email);
205
+ const options = {
206
+ headers: { "User-Agent": "WKD-Checker (+https://miarecki.eu/posts/web-key-directory-setup/)" }
207
+ };
208
+ const [advancedResult, directResult] = yield Promise.all([
209
+ checkKeyUrl(advancedUrl, advancedPolicyUrl, options, email),
210
+ checkKeyUrl(directUrl, directPolicyUrl, options, email)
211
+ ]);
212
+ return { advanced: advancedResult, direct: directResult };
213
+ });
214
+ exports.checkKey = checkKey;
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "wkd-checker",
3
+ "version": "1.1.1",
4
+ "description": "Web Key Directory library for Node.js. Retreive OpenPGP keys from WKD servers and validate them.",
5
+ "author": "Jonatan Miarecki",
6
+ "license": "MIT",
7
+ "devDependencies": {
8
+ "typescript": "^5.4.2"
9
+ },
10
+ "dependencies": {
11
+ "openpgp": "^6.3.0"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts"
22
+ }