redac---n 0.0.0-stage → 0.0.2
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 +21 -0
- package/README.md +323 -2
- package/THIRD_PARTY_NOTICES.md +59 -0
- package/dist/aho.d.ts +25 -0
- package/dist/aho.js +116 -0
- package/dist/base64.d.ts +10 -0
- package/dist/base64.js +29 -0
- package/dist/bip39-data.d.ts +2 -0
- package/dist/bip39-data.js +2 -0
- package/dist/cache.d.ts +14 -0
- package/dist/cache.js +40 -0
- package/dist/calls.d.ts +64 -0
- package/dist/calls.js +269 -0
- package/dist/charlm-data.d.ts +2 -0
- package/dist/charlm-data.js +3 -0
- package/dist/charlm.d.ts +7 -0
- package/dist/charlm.js +65 -0
- package/dist/checksums.d.ts +10 -0
- package/dist/checksums.js +98 -0
- package/dist/commands.d.ts +72 -0
- package/dist/commands.js +787 -0
- package/dist/context.d.ts +98 -0
- package/dist/context.js +469 -0
- package/dist/encoded.d.ts +11 -0
- package/dist/encoded.js +336 -0
- package/dist/environment.d.ts +10 -0
- package/dist/environment.js +62 -0
- package/dist/formats-data.d.ts +2 -0
- package/dist/formats-data.js +405 -0
- package/dist/formats.d.ts +5 -0
- package/dist/formats.js +470 -0
- package/dist/hmac.d.ts +3 -0
- package/dist/hmac.js +87 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +488 -0
- package/dist/json.d.ts +82 -0
- package/dist/json.js +392 -0
- package/dist/known.d.ts +34 -0
- package/dist/known.js +340 -0
- package/dist/lines.d.ts +28 -0
- package/dist/lines.js +69 -0
- package/dist/lookup.d.ts +5 -0
- package/dist/lookup.js +7 -0
- package/dist/model-data.d.ts +3 -0
- package/dist/model-data.js +4 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +19 -0
- package/dist/named.d.ts +20 -0
- package/dist/named.js +3257 -0
- package/dist/names.d.ts +106 -0
- package/dist/names.js +1517 -0
- package/dist/normalize.d.ts +25 -0
- package/dist/normalize.js +529 -0
- package/dist/pem.d.ts +32 -0
- package/dist/pem.js +269 -0
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +470 -0
- package/dist/public.d.ts +44 -0
- package/dist/public.js +362 -0
- package/dist/publickeys.d.ts +41 -0
- package/dist/publickeys.js +232 -0
- package/dist/random.d.ts +29 -0
- package/dist/random.js +165 -0
- package/dist/repeats.d.ts +21 -0
- package/dist/repeats.js +80 -0
- package/dist/rules.d.ts +42 -0
- package/dist/rules.js +58 -0
- package/dist/runs.d.ts +10 -0
- package/dist/runs.js +36 -0
- package/dist/secretfile.d.ts +52 -0
- package/dist/secretfile.js +722 -0
- package/dist/shell.d.ts +127 -0
- package/dist/shell.js +703 -0
- package/dist/spans.d.ts +61 -0
- package/dist/spans.js +287 -0
- package/dist/sql.d.ts +20 -0
- package/dist/sql.js +66 -0
- package/dist/stream.d.ts +46 -0
- package/dist/stream.js +150 -0
- package/dist/structure.d.ts +24 -0
- package/dist/structure.js +1625 -0
- package/dist/syntax.d.ts +156 -0
- package/dist/syntax.js +446 -0
- package/dist/transcript.d.ts +30 -0
- package/dist/transcript.js +713 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +1 -0
- package/dist/unnamed.d.ts +59 -0
- package/dist/unnamed.js +939 -0
- package/dist/urls.d.ts +70 -0
- package/dist/urls.js +303 -0
- package/dist/values.d.ts +178 -0
- package/dist/values.js +1621 -0
- package/dist/yaml.d.ts +40 -0
- package/dist/yaml.js +174 -0
- package/docs/creddata.svg +318 -0
- package/docs/guessing.md +139 -0
- package/docs/holdout.svg +85 -0
- package/docs/layers.md +80 -0
- package/docs/models.svg +81 -0
- package/docs/real-text.svg +112 -0
- package/package.json +57 -5
package/dist/public.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a value can be shown to be public or a stand-in by, whatever found it: keys published for browsers (a
|
|
3
|
+
* publishable key's format, a Google API key used client-side), values published as examples in specifications and
|
|
4
|
+
* documentation, placeholders and masked or truncated keys, JSON Web Tokens anyone can read or sign, and keys passed to
|
|
5
|
+
* browser and mobile SDKs. The detectors
|
|
6
|
+
* consult these to drop stand-ins early; the decision stage (policy.ts) to keep public values whoever proposed them.
|
|
7
|
+
*/
|
|
8
|
+
import { base64Binary } from "./base64.js";
|
|
9
|
+
import { naturalness } from "./charlm.js";
|
|
10
|
+
import { hmacSha256 } from "./hmac.js";
|
|
11
|
+
import { callArguments } from "./calls.js";
|
|
12
|
+
/** Keys that are public by design: publishable, site and project keys meant for browsers. */
|
|
13
|
+
export const PUBLIC_FORMAT = /^(?:pk_(?:live|test)_|NRJS-[0-9a-f]{19}$|rzp_(?:live|test)_[A-Za-z0-9]{14}$|pk\.(?:eyJ|[A-Za-z0-9]{20})|pub[0-9a-f]{32}$|phc_|sb_publishable_|sq0idp-|sqb_[0-9a-f]{40}$)/;
|
|
14
|
+
/** Where the bracket that encloses the end of `before` opens, skipping balanced pairs, or -1. */
|
|
15
|
+
function openingBracket(before) {
|
|
16
|
+
let depth = 0;
|
|
17
|
+
for (let i = before.length - 1; i >= 0; i--) {
|
|
18
|
+
if (before[i] === "}" || before[i] === ")")
|
|
19
|
+
depth++;
|
|
20
|
+
else if (before[i] === "{" || before[i] === "(") {
|
|
21
|
+
if (depth === 0)
|
|
22
|
+
return i;
|
|
23
|
+
depth--;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return -1;
|
|
27
|
+
}
|
|
28
|
+
/** Where the bracket that encloses the start of `after` closes, skipping balanced pairs, or `after.length`. */
|
|
29
|
+
function closingBracket(after) {
|
|
30
|
+
let depth = 0;
|
|
31
|
+
for (let i = 0; i < after.length; i++) {
|
|
32
|
+
if (after[i] === "{" || after[i] === "(")
|
|
33
|
+
depth++;
|
|
34
|
+
else if (after[i] === "}" || after[i] === ")") {
|
|
35
|
+
if (depth === 0)
|
|
36
|
+
return i;
|
|
37
|
+
depth--;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return after.length;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A Google API key in a client app's configuration, where it ships to every user (Google: "API keys for
|
|
44
|
+
* Firebase are not secret"): the object a Firebase web or Flutter config builds, `google-services.json`,
|
|
45
|
+
* the Maps JS loader URL, an Android manifest's or the iOS SDK's Maps key. Elsewhere an `AIza` key is
|
|
46
|
+
* hidden, whatever it's named (`GOOGLE_MAPS_API_KEY=` in a server's environment, a Gemini key): an
|
|
47
|
+
* unrestricted server key costs money. Used by the decision stage, policy.ts.
|
|
48
|
+
*/
|
|
49
|
+
export function isClientGoogleKey(text, start) {
|
|
50
|
+
const before = text.slice(Math.max(0, start - 600), start);
|
|
51
|
+
const line = before.slice(before.lastIndexOf("\n") + 1);
|
|
52
|
+
if (/maps\.googleapis\.com\/maps\/api\/js\?(?:[^\s"'&]*&)*key=$/.test(line))
|
|
53
|
+
return true;
|
|
54
|
+
// A HAR's record of that loader's request: its `queryString` entry, `{"name": "key", "value": "…"}`.
|
|
55
|
+
if (/"url"\s*:\s*"https:\/\/maps\.googleapis\.com\/maps\/api\/js\?[^"]*"(?:(?!"url")[^])*"queryString"\s*:\s*\[[^\]]*\{\s*"name"\s*:\s*"key"\s*,\s*"value"\s*:\s*"$/.test(before))
|
|
56
|
+
return true;
|
|
57
|
+
// Expo's app config, `ios.config.googleMapsApiKey` and `android.config.googleMaps.apiKey`: the Maps SDKs' keys,
|
|
58
|
+
// built into the app.
|
|
59
|
+
if (/\bconfig["']?\s*:\s*\{[^{}]*?(?:["']?googleMapsApiKey|["']?googleMaps["']?\s*:\s*\{[^{}]*?["']?apiKey)["']?\s*:\s*["']$/.test(before))
|
|
60
|
+
return true;
|
|
61
|
+
// An Android manifest's Maps key and the iOS SDK's: the app ships them. (The manifest's within its `<meta-data>` tag,
|
|
62
|
+
// its attributes on one line or several, in either order.)
|
|
63
|
+
if (/GMSServices\.provideAPIKey\(\s*"$|GMSPlacesClient\.provideAPIKey\(\s*"$/.test(line))
|
|
64
|
+
return true;
|
|
65
|
+
const tag = /<meta-data\b[^<>]*android:value="$/.exec(before)?.[0];
|
|
66
|
+
if (tag &&
|
|
67
|
+
(/android:name="com\.google\.android\.geo\.API_KEY"/.test(tag) ||
|
|
68
|
+
/^[^"<>]*"[^<>]*android:name="com\.google\.android\.geo\.API_KEY"/.test(text.slice(start, start + 600))))
|
|
69
|
+
return true;
|
|
70
|
+
// A server's key by name (Gemini, Vertex), whatever is around it.
|
|
71
|
+
if (/server|backend|admin|gemini|genai|generative|vertex|functions:(?:config|secrets)|^[ \t]*export[ \t]/i.test(line))
|
|
72
|
+
return false;
|
|
73
|
+
// A Firebase web config: the object the key sits in has `authDomain` or `messagingSenderId`, or is
|
|
74
|
+
// passed to `initializeApp(` / named `firebaseConfig`; or the line calls the key Firebase's. Not merely
|
|
75
|
+
// a Firebase config nearby (a Gemini key declared next to one is a server key).
|
|
76
|
+
const after = text.slice(start, start + 600);
|
|
77
|
+
const lineAfter = after.split("\n", 1)[0];
|
|
78
|
+
if (/firebase/i.test(line + lineAfter) || /GoogleService-Info|google-services\.json/.test(before))
|
|
79
|
+
return true;
|
|
80
|
+
// The object or call it sits in (`{ … }`, Dart's `FirebaseOptions( … )`), else its paragraph.
|
|
81
|
+
const open = openingBracket(before);
|
|
82
|
+
const close = closingBracket(after);
|
|
83
|
+
let object;
|
|
84
|
+
if (open >= 0)
|
|
85
|
+
object = before.slice(open) + after.slice(0, close);
|
|
86
|
+
else {
|
|
87
|
+
// No enclosing brackets in view (a fragment of a config): its sibling lines, at the key line's indentation.
|
|
88
|
+
const indent = /^[ \t]*/.exec(line)[0];
|
|
89
|
+
const sibling = (l) => l.startsWith(indent) && !/^[ \t]/.test(l.slice(indent.length)) && l.trim() !== "";
|
|
90
|
+
const up = before.split("\n").slice(0, -1).reverse();
|
|
91
|
+
const down = after.split("\n").slice(1);
|
|
92
|
+
const siblings = (ls) => {
|
|
93
|
+
const stop = ls.findIndex((l) => !sibling(l));
|
|
94
|
+
return stop < 0 ? ls : ls.slice(0, stop);
|
|
95
|
+
};
|
|
96
|
+
object = [...siblings(up), line + lineAfter, ...siblings(down)].join("\n");
|
|
97
|
+
}
|
|
98
|
+
// (The bracket must be the config's own: `firebaseConfig = {`, `initializeApp({`, `FirebaseOptions(`.)
|
|
99
|
+
return (/authDomain|messagingSenderId|"api_key"\s*:\s*\[/.test(object) ||
|
|
100
|
+
(open >= 0 &&
|
|
101
|
+
/(?:firebaseConfig\w*\s*[:=]\s*\{|initializeApp\(\s*\{?|FirebaseOptions\(|"api_key"\s*:\s*\[\s*\{)$/.test(before.slice(0, open + 1))));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Example keys from vendor docs and RFCs, which are published on purpose: RFC 6749's authorization code,
|
|
105
|
+
* tokens and client credentials, RFC 6750's bearer token, RFC 7636's PKCE verifier and challenge, RFC 5849's
|
|
106
|
+
* OAuth 1 examples, RFC 6265's and 9110's, and the TOTP secret every authenticator library's docs use. Kept in
|
|
107
|
+
* every syntax (policy.ts).
|
|
108
|
+
*/
|
|
109
|
+
export const PUBLISHED_EXAMPLE = /SplxlOBeZQQYbYS6WxSbIA|2YotnFZFEjr1zCsicMWpAA|tGzv3JOkF0XG5Qx2TlKWIA|s6BhdRkqt3|7Fjfp0ZBr1KtDRbnfVdmIw|gX1fBat3bV|czZCaGRSa3F0MzpnWDFmQmF0M2JW|mF_9\.B5f-4\.1JqM|8xLOxBtZp8|dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk|E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM|hh5s93j4hdidpola|hdk48Djdsa|kd94hf93k423kf44|pfkkdhi9sl3r4s00|nnch734d00sl2jdk|9djdj82h48djs9d2|kkk9d7dh3k39sjv7|j49sk3j29djd|dh893hdasih9|31d4d96e407aad42|aGVsbG86d29ybGQ=|QWxhZGRpbjpvcGVuIHNlc2FtZQ==|JBSWY3DPEHPK3PXP|frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ|GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ/;
|
|
110
|
+
/**
|
|
111
|
+
* Published examples by their whole or a fixed part, found anywhere in a value: the vendors' own (AWS's
|
|
112
|
+
* `AKIAIOSFODNN7EXAMPLE` and the `…EXAMPLE`, `…EXAMPLEKEY` its docs end every key with; Stripe's, GitHub's and jwt.io's
|
|
113
|
+
* documented keys; MySQL's marker of a locked account's password), and filler (`deadbeefdeadbeef`, `EXAMPLEEXAMPLE`).
|
|
114
|
+
* (A documented key's vendor prefix is escaped, `ghp\x5f`, so this source isn't taken for a live key by a scanner.)
|
|
115
|
+
*/
|
|
116
|
+
const EXAMPLES = new RegExp(PUBLISHED_EXAMPLE.source +
|
|
117
|
+
"|" +
|
|
118
|
+
/(?:deadbeef|DEADBEEF|cafebabe|CAFEBABE){2}|SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c|sk\x5ftest_4eC39HqLyjWDarjtT1zdp7dc|ghp\x5f16C7e42F292c6912E7710c838347Ae178B4a|AKI\x41IOSFODNN7|wJalrXUtnFEMI\/K7MDENG|EXAMPLE(?:KEY)?$|(?:EXAMPLE){2}|AbCdEfGh|aBcDeFgH|THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED/
|
|
119
|
+
.source);
|
|
120
|
+
/**
|
|
121
|
+
* Words that say a value stands in for one: `YOUR_API_KEY_HERE`, `my-fake-token`, `INVALID_TOKEN`, `sk_test_xxxxxxxx`,
|
|
122
|
+
* `<redacted>`, `not-a-real-key`, `changeMe`. Whole words, in any of their cases (`There` holds no `here`).
|
|
123
|
+
*/
|
|
124
|
+
const STAND_IN = (() => {
|
|
125
|
+
const words = [
|
|
126
|
+
"fake",
|
|
127
|
+
"invalid",
|
|
128
|
+
"notvalid",
|
|
129
|
+
"dummy",
|
|
130
|
+
"changeme",
|
|
131
|
+
"change[-_ ]?me",
|
|
132
|
+
"replace[-_ ]?me",
|
|
133
|
+
"placeholder",
|
|
134
|
+
"redacted",
|
|
135
|
+
"obscured",
|
|
136
|
+
"masked",
|
|
137
|
+
"hidden",
|
|
138
|
+
"removed",
|
|
139
|
+
"omitted",
|
|
140
|
+
"scrubbed",
|
|
141
|
+
"censored",
|
|
142
|
+
"sanitized",
|
|
143
|
+
"example",
|
|
144
|
+
"your",
|
|
145
|
+
"here",
|
|
146
|
+
"not[-_ ]?a[-_ ]?real",
|
|
147
|
+
"not[-_ ]?real",
|
|
148
|
+
"changed",
|
|
149
|
+
];
|
|
150
|
+
const cases = words.flatMap((w) => [
|
|
151
|
+
w,
|
|
152
|
+
w.toUpperCase(),
|
|
153
|
+
w[0].toUpperCase() + w.slice(1),
|
|
154
|
+
w.replace(/(\])\?me$/, "$1?Me"),
|
|
155
|
+
]);
|
|
156
|
+
// (A word starts after a non-letter or at a camel-case hump, and ends before a non-lower-case letter.)
|
|
157
|
+
return new RegExp(String.raw `(?:(?<![A-Za-z])|(?<=[a-z])(?=[A-Z]))(?:${[...new Set(cases)].join("|")}|[xX]{3,})(?![a-z])`, "g");
|
|
158
|
+
})();
|
|
159
|
+
/** What separates the parts of a placeholder: `YOUR_API_KEY_HERE`, `example-token`, `<your-key>`, `sk-[redacted]`. */
|
|
160
|
+
const PLACEHOLDER_SEPARATORS = /[-_. :/[\]<>(){}*]+/;
|
|
161
|
+
/**
|
|
162
|
+
* A value made of stand-in words and plain ones: each part between separators is a plain one (a small number, a short
|
|
163
|
+
* part like a vendor's `sk_`, `ghp_`, or a word), or holds a stand-in word joined to a word of its own by a case
|
|
164
|
+
* boundary (`exampleToken`) or to the last characters a mask shows (`xxxx1234`). A stand-in word joined to anything
|
|
165
|
+
* else is a weak password built on one, not a placeholder: `Changed2024`, `Example7392`, `ExampleXkqz`,
|
|
166
|
+
* `Fake!Orbit2291`, `invalidXk9mP2vQ7tL4`, `Q7tL4wR8nZ_HERE91kd`.
|
|
167
|
+
*/
|
|
168
|
+
function isStandIn(token) {
|
|
169
|
+
if (token.search(STAND_IN) < 0)
|
|
170
|
+
return false;
|
|
171
|
+
const isWord = (p) => /^[A-Za-z]+$/.test(p) && naturalness(p.toLowerCase()) > 0.3;
|
|
172
|
+
return token
|
|
173
|
+
.split(PLACEHOLDER_SEPARATORS)
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.every((part) => {
|
|
176
|
+
const standIns = [...part.matchAll(STAND_IN)];
|
|
177
|
+
if (!standIns.length)
|
|
178
|
+
return /^\d{1,4}$/.test(part) || /^[A-Za-z]{1,4}$/.test(part) || isWord(part);
|
|
179
|
+
// (The parts around the stand-in words; the mask `xxxx` shows the last 1 to 4 characters after it.)
|
|
180
|
+
let from = 0;
|
|
181
|
+
return [...standIns.map((m) => m.index), part.length].every((to, i) => {
|
|
182
|
+
const joined = part.slice(from, to);
|
|
183
|
+
const mask = /^[xX]+$/.test(standIns[i - 1]?.[0] ?? "");
|
|
184
|
+
from = to + (standIns[i]?.[0].length ?? 0);
|
|
185
|
+
return joined === "" || isWord(joined) || (mask && /^[A-Za-z0-9]{1,4}$/.test(joined));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** A masked key, a few characters shown around the stars: `sk-****abcd`, `ab****yz`. */
|
|
190
|
+
const MASKED = /^[^*\s]{0,8}\*{4,}[^*\s]{0,8}$/;
|
|
191
|
+
/**
|
|
192
|
+
* A token shown cut short, as tools print keys: a vendor's prefix and a few of the key before an ellipsis and a few
|
|
193
|
+
* after (`sk-proj-abc...xyz`, `AKIA…WXYZ`, `...4f2a`), or words after it (`sk-...set-in-1password`). Not a password
|
|
194
|
+
* with an ellipsis in it, a word before it (`Winter...2024`, `Maple...River3012`), or a key with only its front cut
|
|
195
|
+
* off (`...` then 300 characters).
|
|
196
|
+
*/
|
|
197
|
+
export function isTruncated(token) {
|
|
198
|
+
const m = /^([\w+/=:.-]{0,16}?)(?:\.\.\.|…)([\w+/=:.-]{0,40})$/.exec(token);
|
|
199
|
+
if (!m)
|
|
200
|
+
return false;
|
|
201
|
+
const runs = (side) => side.match(/[A-Za-z]+/g) ?? [];
|
|
202
|
+
const isWord = (run) => naturalness(run.toLowerCase()) > 0.3;
|
|
203
|
+
// (A person's word: capitalised, or five letters that read as one; a vendor's prefix is neither: `sk-proj`, `AKIA`.)
|
|
204
|
+
return (!runs(m[1]).some((run) => /^[A-Z][a-z]{3,}$/.test(run) || (run.length >= 5 && isWord(run))) &&
|
|
205
|
+
(m[2].length <= 8 || runs(m[2]).every((run) => run.length <= 2 || isWord(run))));
|
|
206
|
+
}
|
|
207
|
+
/** A template's holes: `{{ secrets.DB }}`, `${API_KEY}`, `%(password)s`, `{db_pass}`, `<password>`. */
|
|
208
|
+
const HOLE = /\{\{[^{}]*\}\}|\$\{[A-Za-z_][\w.:/-]*\}|%\(\w+\)s|\{[a-z_][a-z0-9_.-]{2,60}\}|<[a-z][a-z_ .-]{2,60}>/g;
|
|
209
|
+
/**
|
|
210
|
+
* A stand-in for a key, not one: mostly one repeated character (`sk_test_xxxxxxxx`, `ghp_0000…`, `AKIAXXXX…`; not
|
|
211
|
+
* base64 of bytes with many zeros, `TlRMTVNTUAADAAAAGAAY…`, where the other characters vary), a published
|
|
212
|
+
* example or a placeholder word, a counting sequence, or a template whose literal text is too short or too
|
|
213
|
+
* plain to be a secret on its own (`Bearer ${TOKEN}`, `postgres://${USER}:${PASS}@db:5432/app`; not
|
|
214
|
+
* `{;${EC}4S*A)HbUw`).
|
|
215
|
+
*/
|
|
216
|
+
export function isPlaceholder(token) {
|
|
217
|
+
const counts = new Map();
|
|
218
|
+
for (let i = 0; i < token.length; i++)
|
|
219
|
+
counts.set(token.charCodeAt(i), (counts.get(token.charCodeAt(i)) ?? 0) + 1);
|
|
220
|
+
// (Of the characters that aren't the repeated one or a separator, few: a prefix and the filler.)
|
|
221
|
+
const separators = (token.match(/[-_.:/]/g) ?? []).length;
|
|
222
|
+
for (const n of counts.values())
|
|
223
|
+
if (n >= 6 && n >= token.length * 0.4 && token.length - n - separators <= token.length * 0.35)
|
|
224
|
+
return true;
|
|
225
|
+
if (EXAMPLES.test(token) || isStandIn(token) || isSequential(token) || isTruncated(token) || MASKED.test(token))
|
|
226
|
+
return true;
|
|
227
|
+
if (!/[{<]|%\(/.test(token))
|
|
228
|
+
return false;
|
|
229
|
+
// (A hole in a docs' syntax, `<…>` or `{…}`, names something: `<password>`, not `<qjk>`.)
|
|
230
|
+
const rest = token.replace(HOLE, (h) => /^[<{][^{$]/.test(h) && naturalness(h.slice(1, -1).replace(/[_ .-]+/g, " ")) <= 0.5 ? h : "");
|
|
231
|
+
if (rest === token)
|
|
232
|
+
return false;
|
|
233
|
+
const letters = rest.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
234
|
+
return rest.replace(/\s/g, "").length < 8 || naturalness(letters) > 0.5;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Counting sequences, after a vendor's prefix: `abcdef123456`, `sk_test_1234567890abcdef`, hex test vectors like
|
|
238
|
+
* `000102030405…` or `fffefdfc…`. Four in five characters (or bytes) count; a person's password made of a run and a word
|
|
239
|
+
* (`123456789Maple6543%`) is a password.
|
|
240
|
+
*/
|
|
241
|
+
function isSequential(token) {
|
|
242
|
+
const body = token.replace(/^(?:[A-Za-z]{1,8}[_-])+/, "");
|
|
243
|
+
if (body.length < 8)
|
|
244
|
+
return false;
|
|
245
|
+
// Characters inside runs of 4+ counting steps, one step all the way (`abcd`, `4321`, `aaaa`; not `9887`).
|
|
246
|
+
let inRuns = 0;
|
|
247
|
+
let run = 1;
|
|
248
|
+
let step = 2;
|
|
249
|
+
for (let i = 1; i <= body.length; i++) {
|
|
250
|
+
const next = i < body.length ? body.charCodeAt(i) - body.charCodeAt(i - 1) : 2;
|
|
251
|
+
if (Math.abs(next) <= 1 && (run === 1 || next === step))
|
|
252
|
+
run++;
|
|
253
|
+
else {
|
|
254
|
+
if (run >= 4)
|
|
255
|
+
inRuns += run;
|
|
256
|
+
// (The pair that broke the run may start the next: `aaab`… `bcd`.)
|
|
257
|
+
run = Math.abs(next) <= 1 ? 2 : 1;
|
|
258
|
+
}
|
|
259
|
+
step = next;
|
|
260
|
+
}
|
|
261
|
+
if (inRuns >= body.length * 0.8)
|
|
262
|
+
return true;
|
|
263
|
+
// Hex as bytes, the same way: bytes inside runs of 4+ that step by 0, 1 or 0x11, one step all the way (`00010203`,
|
|
264
|
+
// `aabbccdd`, `fffefdfc`). (`7f3b9c2e1d4a5b6c` has steps of 0x11 here and there, not runs: a key. Digits alone step
|
|
265
|
+
// by 0x11 as a number's pairs do, `12233445`, `98879887`: a PIN, not bytes counting.)
|
|
266
|
+
if (/^[0-9a-f]+$/i.test(body) && body.length % 2 === 0) {
|
|
267
|
+
const strides = /[a-f]/i.test(body) ? [0, 1, 0x11] : [0, 1];
|
|
268
|
+
const n = body.length / 2;
|
|
269
|
+
let inByteRuns = 0;
|
|
270
|
+
let byteRun = 1;
|
|
271
|
+
let step = NaN;
|
|
272
|
+
for (let i = 1; i <= n; i++) {
|
|
273
|
+
const d = i < n ? parseInt(body.slice(i * 2, i * 2 + 2), 16) - parseInt(body.slice(i * 2 - 2, i * 2), 16) : NaN;
|
|
274
|
+
if (strides.includes(Math.abs(d)) && (byteRun === 1 || d === step))
|
|
275
|
+
byteRun++;
|
|
276
|
+
else {
|
|
277
|
+
if (byteRun >= 4)
|
|
278
|
+
inByteRuns += byteRun;
|
|
279
|
+
// (The pair that broke the run may start the next.)
|
|
280
|
+
byteRun = strides.includes(Math.abs(d)) ? 2 : 1;
|
|
281
|
+
}
|
|
282
|
+
step = d;
|
|
283
|
+
}
|
|
284
|
+
return inByteRuns >= n * 0.8;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* A JWT meant to be public or published: Supabase anon keys, the jwt.io and RFC samples. With
|
|
290
|
+
* `samplesOnly`, only the published samples (an anon key assigned to a name is still hidden).
|
|
291
|
+
*/
|
|
292
|
+
export function isPublicJwt(token, samplesOnly = false) {
|
|
293
|
+
if (!token.startsWith("eyJ"))
|
|
294
|
+
return false;
|
|
295
|
+
const [head, body] = token.split(".");
|
|
296
|
+
const payload = base64Binary(body ?? "");
|
|
297
|
+
if (!payload)
|
|
298
|
+
return false;
|
|
299
|
+
// (jwt.io's sample claims say a sample when a public key signed them, RS256's and the others'; an HMAC token is a
|
|
300
|
+
// sample only by its docs key, signedWithDocsKey, as one signed with a real secret may copy the claims.)
|
|
301
|
+
const hmac = /"alg"\s*:\s*"HS\d+"/.test(base64Binary(head) ?? "");
|
|
302
|
+
return ((!samplesOnly && /"role"\s*:\s*"anon"/.test(payload)) ||
|
|
303
|
+
(!hmac && (/"sub"\s*:\s*"1234567890"/.test(payload) || /"name"\s*:\s*"John Doe"/.test(payload))) ||
|
|
304
|
+
signedWithDocsKey(token));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* The keys docs and tutorials sign their example HS256 tokens with, each one spelled as a stand-in: jwt.io's, the
|
|
308
|
+
* jsonwebtoken README's and express-session's.
|
|
309
|
+
*/
|
|
310
|
+
const DOCS_JWT_KEYS = ["your-256-bit-secret", "your-secret-key", "your_jwt_secret", "shhhhh", "keyboard cat"];
|
|
311
|
+
/**
|
|
312
|
+
* The weak keys both docs and apps in development sign with. A token signed with one is a sample only when its claims
|
|
313
|
+
* say nothing of a session (PyJWT's `{"a": "b"}`, `{"some": "payload"}`); one with a subject, a user or a time is that
|
|
314
|
+
* app's bearer credential.
|
|
315
|
+
*/
|
|
316
|
+
const WEAK_JWT_KEYS = ["secret", "test", "key", "changeme", "password"];
|
|
317
|
+
/** Claims a session's token carries: a subject, a user, an expiry or an issue time. */
|
|
318
|
+
const SESSION_CLAIM = /"(?:sub|exp|iat|nbf|jti|uid|user(?:_?id|name)?|email|role|scope|sid|session(?:_?id)?)"\s*:/i;
|
|
319
|
+
/**
|
|
320
|
+
* Whether an HS256 JWT's signature verifies under a docs key (DOCS_JWT_KEYS), or under a weak key with no session
|
|
321
|
+
* claims (WEAK_JWT_KEYS): anyone can mint it, so it's a sample.
|
|
322
|
+
*/
|
|
323
|
+
function signedWithDocsKey(token) {
|
|
324
|
+
const [header, payload, signature] = token.split(".");
|
|
325
|
+
if (!signature || !/"alg"\s*:\s*"HS256"/.test(base64Binary(header) ?? ""))
|
|
326
|
+
return false;
|
|
327
|
+
const utf8 = new TextEncoder();
|
|
328
|
+
const message = utf8.encode(`${header}.${payload}`);
|
|
329
|
+
const keys = SESSION_CLAIM.test(base64Binary(payload) ?? "") ? DOCS_JWT_KEYS : [...DOCS_JWT_KEYS, ...WEAK_JWT_KEYS];
|
|
330
|
+
return keys.some((key) => base64url(hmacSha256(utf8.encode(key), message)) === signature);
|
|
331
|
+
}
|
|
332
|
+
const base64url = (bytes) => btoa(String.fromCharCode(...bytes))
|
|
333
|
+
.replace(/\+/g, "-")
|
|
334
|
+
.replace(/\//g, "_")
|
|
335
|
+
.replace(/=+$/, "");
|
|
336
|
+
/**
|
|
337
|
+
* Browser and mobile SDK initialisers, whose keys are public by design: calls (`Sentry.init(`, `new Pusher(`,
|
|
338
|
+
* `loadStripe(`) or config objects (`firebaseConfig = {`, `_rollbarConfig = {`). Not a vendor word alone:
|
|
339
|
+
* `IntercomClient(…)` is a server SDK, a bare `createInstance(` is any factory unless Optimizely's is imported (see
|
|
340
|
+
* clientSdkAt), and a comment naming the vendor initialises nothing.
|
|
341
|
+
*/
|
|
342
|
+
const CLIENT_SDK_CALL = /(?:\b(?:AnalyticsBrowser\.load|analytics\.load|Bugsnag\.start|Rollbar\.init|optimizely(?:Sdk)?\.createInstance|createInstance|(?:new\s+)?GrowthBook|Purchases\.configure|braintree\.\w+\.create|(?:new\s+)?AdyenCheckout|branch\.init|initializeApp|new\s+mapboxgl\.\w+|(?:new\s+)?Pusher|Sentry\.init|posthog\.init|amplitude\.(?:init|getInstance\(\)\.init)|mixpanel\.init|LDClient\.initialize|datadogRum\.init|DD_RUM\.init|heap\.load|algoliasearch|docsearch|instantsearch|Intercom|loadStripe|Stripe|Square\.payments)\s*\(|\b(?:_rollbarConfig|firebaseConfig\w*)\s*[:=]\s*\{)/g;
|
|
343
|
+
/**
|
|
344
|
+
* The client SDK initialiser whose arguments `at` is directly inside, or inside an object or list given as one of them,
|
|
345
|
+
* if the call starts within 300 characters before it: its call as written (`algoliasearch(`).
|
|
346
|
+
*/
|
|
347
|
+
export function clientSdkAt(text, at) {
|
|
348
|
+
const from = Math.max(0, at - 300);
|
|
349
|
+
let last;
|
|
350
|
+
for (const m of text.slice(from, at).matchAll(CLIENT_SDK_CALL))
|
|
351
|
+
last = m;
|
|
352
|
+
if (!last || (/^createInstance/.test(last[0]) && !/@optimizely\//.test(text.slice(0, at))))
|
|
353
|
+
return undefined;
|
|
354
|
+
const holding = (args) => args?.find((a) => a.start <= at && at < a.end);
|
|
355
|
+
const arg = holding(callArguments(text, from + last.index + last[0].length));
|
|
356
|
+
if (!arg)
|
|
357
|
+
return undefined;
|
|
358
|
+
const inside = arg.literal || at === arg.start || (/^[{[]/.test(arg.text) && holding(callArguments(text, arg.start + 1)));
|
|
359
|
+
return inside ? last[0] : undefined;
|
|
360
|
+
}
|
|
361
|
+
/** The parameters client SDKs take public keys under (positional arguments count too). */
|
|
362
|
+
export const PUBLIC_SDK_PARAM = /^(?:api_?key|key|client_?key|client_?token|sdk_?key|app_?id|application_?id|dsn|authorization|publishable_?key|public_?key|site_?key|access_?token|token|search_?(?:only_?)?api_?key)$/i;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ArmoredBlock } from "./pem.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A SubjectPublicKeyInfo: a SEQUENCE whose first element is the AlgorithmIdentifier, a SEQUENCE that starts with a
|
|
4
|
+
* public key algorithm's OID. (A private key's PKCS #8 and PKCS #1 encodings start with an INTEGER version instead.)
|
|
5
|
+
*/
|
|
6
|
+
export declare function isDerPublicKey(b64: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* An X.509 certificate's DER (what `-----BEGIN CERTIFICATE-----` holds, `MIIDdzCCAl+gAwIBAgI…`): a SEQUENCE whose first
|
|
9
|
+
* element, the certificate to be signed, opens with its version (`[0]` holding an INTEGER of 0 to 2) and an INTEGER
|
|
10
|
+
* serial number of 1 to 21 bytes, then the signature algorithm's SEQUENCE.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isDerCertificate(b64: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A private key's DER, whatever armor it's in: a SEQUENCE whose first element is a one-byte INTEGER version, 0 to 2
|
|
15
|
+
* (PKCS #1's RSAPrivateKey and PKCS #8's PrivateKeyInfo start with 0, OneAsymmetricKey with 1, SEC 1's EC key with
|
|
16
|
+
* 1). No public structure starts so: a SubjectPublicKeyInfo or a certificate opens with a SEQUENCE, an RSAPublicKey
|
|
17
|
+
* with its modulus. (SEC 1's explicit ECParameters start with version 1 too, then a field's SEQUENCE and the curve's:
|
|
18
|
+
* PKCS #8 has its key's OCTET STRING after the algorithm's SEQUENCE.)
|
|
19
|
+
*/
|
|
20
|
+
export declare function isDerPrivateKey(b64: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* A PKCS #1 RSAPublicKey (`-----BEGIN RSA PUBLIC KEY-----`, `MIIBCgKCAQEA…`, `MIGJAoGBA…` for 1,024 bits): a SEQUENCE
|
|
23
|
+
* of the modulus, 1,024 to 8,192 bits in steps of 512 with its top bit set, and an exponent of 1 to 4 bytes, the SEQUENCE's length the
|
|
24
|
+
* two's.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isRsaPublicKey(b64: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* A DER ECDSA signature, whole: a SEQUENCE of two INTEGERs, r and s, of 1 to 66 bytes (P-256 to P-521; a leading zero
|
|
29
|
+
* only before a top bit), the SEQUENCE's length theirs and the value's length the SEQUENCE's.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isDerSignature(b64: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* An OpenPGP v4 public key or public subkey packet (tag 6 or 14), in the old or the new packet format: a creation
|
|
34
|
+
* time since 1995 and not in the future, then an RSA modulus whose bit count agrees with its first byte, or an
|
|
35
|
+
* elliptic curve's OID.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isPgpPublicKey(b64: string): boolean;
|
|
38
|
+
/** The public armored blocks of a text (PUBLIC_LABEL's), in order. */
|
|
39
|
+
export declare const publicBlocks: (text: string) => ArmoredBlock[];
|
|
40
|
+
/** Whether PEM text is public through and through: it has a BEGIN line, and every one is public armor (a bundle with a key isn't). */
|
|
41
|
+
export declare function isPublicPem(pem: string): boolean;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public keys recognised by their encoding's structure, from the first bytes of their base64: a DER
|
|
3
|
+
* SubjectPublicKeyInfo (what `-----BEGIN PUBLIC KEY-----` holds) and an OpenPGP public key packet (an armored
|
|
4
|
+
* `PUBLIC KEY BLOCK`'s body). Each check reads enough fields that agree with each other (a known algorithm's OID, a
|
|
5
|
+
* key size and the bit it sets, a packet length that is its fields' sum) that random base64 passes with odds below one
|
|
6
|
+
* in a hundred billion: a value that passes is kept. (Measured for PGP: about 1 random value in 10^8 has the header,
|
|
7
|
+
* version, creation time and key size, and 1 in 14,000 of those the length.)
|
|
8
|
+
*/
|
|
9
|
+
import { base64Bytes, base64Prefix } from "./base64.js";
|
|
10
|
+
import { armoredBlocks, beginLines } from "./pem.js";
|
|
11
|
+
/** How much base64 text the checks read: 48 bytes cover every header they look at. */
|
|
12
|
+
const HEAD_CHARS = 64;
|
|
13
|
+
/** The first bytes base64 text encodes, or an empty array. */
|
|
14
|
+
function head(b64) {
|
|
15
|
+
return base64Prefix(b64.slice(0, HEAD_CHARS)) ?? new Uint8Array();
|
|
16
|
+
}
|
|
17
|
+
const hex = (b, from, length) => Array.from(b.subarray(from, from + length), (x) => x.toString(16).padStart(2, "0")).join("");
|
|
18
|
+
/** Where a DER element's content starts, given where its tag is, and how long it is; or undefined for an invalid length. */
|
|
19
|
+
function derElement(b, at) {
|
|
20
|
+
const first = b[at + 1];
|
|
21
|
+
if (first === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (first < 0x80)
|
|
24
|
+
return { content: at + 2, length: first };
|
|
25
|
+
const size = first & 0x7f;
|
|
26
|
+
if (size < 1 || size > 3 || b.length < at + 2 + size)
|
|
27
|
+
return undefined;
|
|
28
|
+
let length = 0;
|
|
29
|
+
for (let i = 0; i < size; i++)
|
|
30
|
+
length = length * 256 + b[at + 2 + i];
|
|
31
|
+
return { content: at + 2 + size, length };
|
|
32
|
+
}
|
|
33
|
+
/** Where a DER element's content starts, given where its tag is, or -1 for an invalid length. */
|
|
34
|
+
function derContent(b, at) {
|
|
35
|
+
return derElement(b, at)?.content ?? -1;
|
|
36
|
+
}
|
|
37
|
+
/** Public key algorithms' OIDs: RSA, RSASSA-PSS, DSA, DH, EC, Ed25519, Ed448, X25519, X448. */
|
|
38
|
+
const KEY_ALGORITHMS = new Set([
|
|
39
|
+
"2a864886f70d010101",
|
|
40
|
+
"2a864886f70d01010a",
|
|
41
|
+
"2a8648ce380401",
|
|
42
|
+
"2a864886f70d010301",
|
|
43
|
+
"2a8648ce3d0201",
|
|
44
|
+
"2b6570",
|
|
45
|
+
"2b6571",
|
|
46
|
+
"2b656e",
|
|
47
|
+
"2b656f",
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* A SubjectPublicKeyInfo: a SEQUENCE whose first element is the AlgorithmIdentifier, a SEQUENCE that starts with a
|
|
51
|
+
* public key algorithm's OID. (A private key's PKCS #8 and PKCS #1 encodings start with an INTEGER version instead.)
|
|
52
|
+
*/
|
|
53
|
+
export function isDerPublicKey(b64) {
|
|
54
|
+
const b = head(b64);
|
|
55
|
+
if (b[0] !== 0x30)
|
|
56
|
+
return false;
|
|
57
|
+
const inner = derContent(b, 0);
|
|
58
|
+
if (inner < 0 || b[inner] !== 0x30)
|
|
59
|
+
return false;
|
|
60
|
+
const oid = derContent(b, inner);
|
|
61
|
+
return oid >= 0 && b[oid] === 0x06 && KEY_ALGORITHMS.has(hex(b, oid + 2, b[oid + 1]));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* An X.509 certificate's DER (what `-----BEGIN CERTIFICATE-----` holds, `MIIDdzCCAl+gAwIBAgI…`): a SEQUENCE whose first
|
|
65
|
+
* element, the certificate to be signed, opens with its version (`[0]` holding an INTEGER of 0 to 2) and an INTEGER
|
|
66
|
+
* serial number of 1 to 21 bytes, then the signature algorithm's SEQUENCE.
|
|
67
|
+
*/
|
|
68
|
+
export function isDerCertificate(b64) {
|
|
69
|
+
const b = head(b64);
|
|
70
|
+
if (b[0] !== 0x30)
|
|
71
|
+
return false;
|
|
72
|
+
const tbs = derContent(b, 0);
|
|
73
|
+
if (tbs < 0 || b[tbs] !== 0x30)
|
|
74
|
+
return false;
|
|
75
|
+
const version = derContent(b, tbs);
|
|
76
|
+
if (version < 0 || hex(b, version, 4) !== "a0030201" || b[version + 4] > 2)
|
|
77
|
+
return false;
|
|
78
|
+
const serial = version + 5;
|
|
79
|
+
return b[serial] === 0x02 && b[serial + 1] >= 1 && b[serial + 1] <= 21 && b[serial + 2 + b[serial + 1]] === 0x30;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A private key's DER, whatever armor it's in: a SEQUENCE whose first element is a one-byte INTEGER version, 0 to 2
|
|
83
|
+
* (PKCS #1's RSAPrivateKey and PKCS #8's PrivateKeyInfo start with 0, OneAsymmetricKey with 1, SEC 1's EC key with
|
|
84
|
+
* 1). No public structure starts so: a SubjectPublicKeyInfo or a certificate opens with a SEQUENCE, an RSAPublicKey
|
|
85
|
+
* with its modulus. (SEC 1's explicit ECParameters start with version 1 too, then a field's SEQUENCE and the curve's:
|
|
86
|
+
* PKCS #8 has its key's OCTET STRING after the algorithm's SEQUENCE.)
|
|
87
|
+
*/
|
|
88
|
+
export function isDerPrivateKey(b64) {
|
|
89
|
+
const b = head(b64);
|
|
90
|
+
if (b[0] !== 0x30)
|
|
91
|
+
return false;
|
|
92
|
+
const inner = derContent(b, 0);
|
|
93
|
+
if (inner < 0 || b[inner] !== 0x02 || b[inner + 1] !== 0x01 || b[inner + 2] > 2)
|
|
94
|
+
return false;
|
|
95
|
+
if (b[inner + 3] !== 0x30)
|
|
96
|
+
return true;
|
|
97
|
+
const algorithm = derElement(b, inner + 3);
|
|
98
|
+
const next = algorithm ? algorithm.content + algorithm.length : -1;
|
|
99
|
+
return next >= b.length || b[next] === 0x04;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A PKCS #1 RSAPublicKey (`-----BEGIN RSA PUBLIC KEY-----`, `MIIBCgKCAQEA…`, `MIGJAoGBA…` for 1,024 bits): a SEQUENCE
|
|
103
|
+
* of the modulus, 1,024 to 8,192 bits in steps of 512 with its top bit set, and an exponent of 1 to 4 bytes, the SEQUENCE's length the
|
|
104
|
+
* two's.
|
|
105
|
+
*/
|
|
106
|
+
export function isRsaPublicKey(b64) {
|
|
107
|
+
const b = head(b64);
|
|
108
|
+
const sequence = derElement(b, 0);
|
|
109
|
+
const modulus = sequence && derElement(b, sequence.content);
|
|
110
|
+
if (b[0] !== 0x30 || !sequence || !modulus || b[sequence.content] !== 0x02)
|
|
111
|
+
return false;
|
|
112
|
+
const exponent = sequence.length - (modulus.content - sequence.content) - modulus.length - 2; // (the exponent's tag and length)
|
|
113
|
+
return (modulus.length >= 129 &&
|
|
114
|
+
modulus.length <= 1025 &&
|
|
115
|
+
modulus.length % 64 === 1 &&
|
|
116
|
+
b[modulus.content] === 0 &&
|
|
117
|
+
b[modulus.content + 1] >= 0x80 &&
|
|
118
|
+
exponent >= 1 &&
|
|
119
|
+
exponent <= 4);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A DER ECDSA signature, whole: a SEQUENCE of two INTEGERs, r and s, of 1 to 66 bytes (P-256 to P-521; a leading zero
|
|
123
|
+
* only before a top bit), the SEQUENCE's length theirs and the value's length the SEQUENCE's.
|
|
124
|
+
*/
|
|
125
|
+
export function isDerSignature(b64) {
|
|
126
|
+
if (b64.length > 200)
|
|
127
|
+
return false;
|
|
128
|
+
const b = base64Bytes(b64) ?? new Uint8Array();
|
|
129
|
+
const content = derContent(b, 0);
|
|
130
|
+
if (b[0] !== 0x30 || content < 0 || content + (b[1] < 0x80 ? b[1] : b[2]) !== b.length)
|
|
131
|
+
return false;
|
|
132
|
+
const integer = (at) => b[at] === 0x02 && b[at + 1] >= 1 && b[at + 1] <= 66 && (b[at + 2] !== 0 || b[at + 3] >= 0x80);
|
|
133
|
+
const s = content + 2 + b[content + 1];
|
|
134
|
+
return integer(content) && integer(s) && s + 2 + b[s + 1] === b.length;
|
|
135
|
+
}
|
|
136
|
+
/** OpenPGP's elliptic curves by OID: NIST P-256, P-384, P-521, brainpool, secp256k1, Ed25519, Curve25519, Ed448, X448. */
|
|
137
|
+
const PGP_CURVES = new Set([
|
|
138
|
+
"2a8648ce3d030107",
|
|
139
|
+
"2b81040022",
|
|
140
|
+
"2b81040023",
|
|
141
|
+
"2b2403030208010107",
|
|
142
|
+
"2b240303020801010b",
|
|
143
|
+
"2b240303020801010d",
|
|
144
|
+
"2b8104000a",
|
|
145
|
+
"2b06010401da470f01",
|
|
146
|
+
"2b060104019755010501",
|
|
147
|
+
"2b6571",
|
|
148
|
+
"2b656f",
|
|
149
|
+
]);
|
|
150
|
+
/** Seconds since 1970 of 1995-01-01: OpenPGP keys made before it don't turn up. */
|
|
151
|
+
const PGP_EPOCH = 788_918_400;
|
|
152
|
+
/**
|
|
153
|
+
* An OpenPGP v4 public key or public subkey packet (tag 6 or 14), in the old or the new packet format: a creation
|
|
154
|
+
* time since 1995 and not in the future, then an RSA modulus whose bit count agrees with its first byte, or an
|
|
155
|
+
* elliptic curve's OID.
|
|
156
|
+
*/
|
|
157
|
+
export function isPgpPublicKey(b64) {
|
|
158
|
+
const b = head(b64);
|
|
159
|
+
if (b.length < 16)
|
|
160
|
+
return false;
|
|
161
|
+
let tag;
|
|
162
|
+
let body;
|
|
163
|
+
let length;
|
|
164
|
+
if ((b[0] & 0xc0) === 0x80) {
|
|
165
|
+
// Old format: the tag in bits 5–2, the length's size in bits 1–0 (1, 2 or 4 octets).
|
|
166
|
+
tag = (b[0] >> 2) & 0x0f;
|
|
167
|
+
const size = [1, 2, 4][b[0] & 3];
|
|
168
|
+
if (size === undefined)
|
|
169
|
+
return false;
|
|
170
|
+
body = 1 + size;
|
|
171
|
+
length = 0;
|
|
172
|
+
for (let i = 1; i < body; i++)
|
|
173
|
+
length = length * 256 + b[i];
|
|
174
|
+
}
|
|
175
|
+
else if ((b[0] & 0xc0) === 0xc0) {
|
|
176
|
+
// New format: the tag in bits 5–0, a length of 1, 2 or 5 octets.
|
|
177
|
+
tag = b[0] & 0x3f;
|
|
178
|
+
body = b[1] < 192 ? 2 : b[1] < 224 ? 3 : b[1] === 255 ? 6 : -1;
|
|
179
|
+
if (body < 0)
|
|
180
|
+
return false;
|
|
181
|
+
length =
|
|
182
|
+
body === 2
|
|
183
|
+
? b[1]
|
|
184
|
+
: body === 3
|
|
185
|
+
? ((b[1] - 192) << 8) + b[2] + 192
|
|
186
|
+
: ((b[2] << 24) | (b[3] << 16) | (b[4] << 8) | b[5]) >>> 0;
|
|
187
|
+
}
|
|
188
|
+
else
|
|
189
|
+
return false;
|
|
190
|
+
if ((tag !== 6 && tag !== 14) || b[body] !== 4)
|
|
191
|
+
return false;
|
|
192
|
+
const created = ((b[body + 1] << 24) | (b[body + 2] << 16) | (b[body + 3] << 8) | b[body + 4]) >>> 0;
|
|
193
|
+
if (created < PGP_EPOCH || created > Date.now() / 1000 + 86_400)
|
|
194
|
+
return false;
|
|
195
|
+
const algorithm = b[body + 5];
|
|
196
|
+
// (The packet's length is its fields': version, time and algorithm, 6 octets, then the key's.)
|
|
197
|
+
if (algorithm === 1 || algorithm === 2 || algorithm === 3) {
|
|
198
|
+
// RSA: the modulus is an MPI, its bit count (1,024 to 16,384) then its bytes, the first holding its top bit; then
|
|
199
|
+
// the exponent's MPI, of 1 to 4 bytes (65537 takes 3).
|
|
200
|
+
const bits = (b[body + 6] << 8) | b[body + 7];
|
|
201
|
+
const top = b[body + 8];
|
|
202
|
+
const exponent = length - 6 - 2 - Math.ceil(bits / 8) - 2;
|
|
203
|
+
return (bits >= 1024 &&
|
|
204
|
+
bits <= 16_384 &&
|
|
205
|
+
top !== undefined &&
|
|
206
|
+
31 - Math.clz32(top) === (bits - 1) % 8 &&
|
|
207
|
+
exponent >= 1 &&
|
|
208
|
+
exponent <= 4);
|
|
209
|
+
}
|
|
210
|
+
// ECDH, ECDSA, EdDSA: the curve's OID, after its length; the point's MPI; for ECDH its KDF parameters, 4 octets.
|
|
211
|
+
if (algorithm !== 18 && algorithm !== 19 && algorithm !== 22)
|
|
212
|
+
return false;
|
|
213
|
+
const oid = b[body + 6];
|
|
214
|
+
const at = body + 7 + oid;
|
|
215
|
+
if (!PGP_CURVES.has(hex(b, body + 7, oid)) || at + 1 >= b.length)
|
|
216
|
+
return false;
|
|
217
|
+
const point = (b[at] << 8) | b[at + 1];
|
|
218
|
+
return length === 6 + 1 + oid + 2 + Math.ceil(point / 8) + (algorithm === 18 ? 4 : 0);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The labels of PEM armor that holds public material or ciphertext, neither of them a secret to redact: certificates
|
|
222
|
+
* and their requests, CRLs, public keys, PGP public blocks and signatures, parameters; and `ENCRYPTED FILE`,
|
|
223
|
+
* `PGP MESSAGE`, kept as encrypted values are.
|
|
224
|
+
*/
|
|
225
|
+
const PUBLIC_LABEL = /^(?:[A-Z0-9]+ )*(?:PUBLIC KEY|CERTIFICATE|X509 CRL|CERTIFICATE REQUEST|NEW CERTIFICATE REQUEST|PGP PUBLIC KEY BLOCK|PGP SIGNATURE|PGP MESSAGE|ENCRYPTED FILE|SSH2 PUBLIC KEY|DH PARAMETERS|EC PARAMETERS|PKCS7|CMS)$/;
|
|
226
|
+
/** The public armored blocks of a text (PUBLIC_LABEL's), in order. */
|
|
227
|
+
export const publicBlocks = (text) => armoredBlocks(text).filter((b) => PUBLIC_LABEL.test(b.label));
|
|
228
|
+
/** Whether PEM text is public through and through: it has a BEGIN line, and every one is public armor (a bundle with a key isn't). */
|
|
229
|
+
export function isPublicPem(pem) {
|
|
230
|
+
const labels = [...beginLines(pem)].map((b) => b.label);
|
|
231
|
+
return labels.length > 0 && labels.every((label) => PUBLIC_LABEL.test(label));
|
|
232
|
+
}
|
package/dist/random.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measures of how random a token looks, from the token alone, for the random layer's model (unnamed.ts): the
|
|
3
|
+
* alphabet it is drawn from, its entropy as a z-score against a uniformly random string of the same length over that
|
|
4
|
+
* alphabet (so a 20-character hex string and a 60-character base64 one are judged on one scale), and its character
|
|
5
|
+
* classes and how often they switch. A random key, a hash and an encrypted blob all look random here; telling them
|
|
6
|
+
* apart is the context's job.
|
|
7
|
+
*/
|
|
8
|
+
export type Alphabet = "hex" | "HEX" | "b32" | "b36" | "B36" | "b62" | "b64" | "b64url" | "other";
|
|
9
|
+
/** The smallest common alphabet that contains every character of the token. (`=` is base64's padding, in none.) */
|
|
10
|
+
export declare function alphabetOf(token: string): Alphabet;
|
|
11
|
+
/** Shannon entropy of the characters, in bits per character. */
|
|
12
|
+
export declare function entropy(token: string): number;
|
|
13
|
+
/**
|
|
14
|
+
* How many standard deviations the token's entropy sits from a random string's. Random strings score near 0.
|
|
15
|
+
* (Measured over the first 512 characters, the longest string randomEntropy simulates.)
|
|
16
|
+
*/
|
|
17
|
+
export declare function entropyZ(token: string, alphabet?: Alphabet): number;
|
|
18
|
+
/** A token's character classes: how many of each, and how often the class changes. */
|
|
19
|
+
export interface Shape {
|
|
20
|
+
length: number;
|
|
21
|
+
lower: number;
|
|
22
|
+
upper: number;
|
|
23
|
+
digit: number;
|
|
24
|
+
/** Changes between letter case, digit and symbol, per character boundary. */
|
|
25
|
+
switches: number;
|
|
26
|
+
/** What `switches` would be for a random string with these class proportions. */
|
|
27
|
+
expectedSwitches: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function shape(token: string): Shape;
|