-
-
Notifications
You must be signed in to change notification settings - Fork 188
/
passkeys.js
210 lines (178 loc) · 7.8 KB
/
passkeys.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
'use strict';
const PASSKEYS_ATTESTATION_NOT_SUPPORTED = 20;
const PASSKEYS_CREDENTIAL_IS_EXCLUDED = 21;
const PASSKEYS_REQUEST_CANCELED = 22;
const PASSKEYS_INVALID_USER_VERIFICATION = 23;
const PASSKEYS_EMPTY_PUBLIC_KEY = 24;
const PASSKEYS_INVALID_URL_PROVIDED = 25;
const PASSKEYS_ORIGIN_NOT_ALLOWED = 26;
const PASSKEYS_DOMAIN_IS_NOT_VALID = 27;
const PASSKEYS_DOMAIN_RPID_MISMATCH = 28;
const PASSKEYS_NO_SUPPORTED_ALGORITHMS = 29;
const PASSKEYS_WAIT_FOR_LIFETIMER = 30;
const PASSKEYS_UNKNOWN_ERROR = 31;
const PASSKEYS_INVALID_CHALLENGE = 32;
const PASSKEYS_INVALID_USER_ID = 33;
const stringToArrayBuffer = function(str) {
const arr = Uint8Array.from(str, c => c.charCodeAt(0));
return arr.buffer;
};
// From URL encoded base64 string to ArrayBuffer
const base64ToArrayBuffer = function(str) {
return stringToArrayBuffer(window.atob(str?.replaceAll('-', '+').replaceAll('_', '/')));
};
// Wraps response to AuthenticatorAttestationResponse object
const createAttestationResponse = function(publicKey) {
const response = {
attestationObject: base64ToArrayBuffer(publicKey.response.attestationObject),
clientDataJSON: base64ToArrayBuffer(publicKey.response.clientDataJSON),
getAuthenticatorData: () => base64ToArrayBuffer(publicKey.response?.authenticatorData),
getTransports: () => [ 'internal' ]
};
return Object.setPrototypeOf(response, AuthenticatorAttestationResponse.prototype);
};
// Wraps response to AuthenticatorAssertionResponse object
const createAssertionResponse = function(publicKey) {
const response = {
authenticatorData: base64ToArrayBuffer(publicKey.response?.authenticatorData),
clientDataJSON: base64ToArrayBuffer(publicKey.response?.clientDataJSON),
signature: base64ToArrayBuffer(publicKey.response?.signature),
userHandle: publicKey.response?.userHandle ? base64ToArrayBuffer(publicKey.response?.userHandle) : null
};
return Object.setPrototypeOf(response, AuthenticatorAssertionResponse.prototype);
};
// Wraps public key to PublicKeyCredential object
const createPublicKeyCredential = function(publicKey) {
const authenticatorResponse = publicKey?.response?.attestationObject
? createAttestationResponse(publicKey)
: createAssertionResponse(publicKey);
const publicKeyCredential = {
authenticatorAttachment: publicKey.authenticatorAttachment,
id: publicKey.id,
rawId: base64ToArrayBuffer(publicKey.id),
response: authenticatorResponse,
type: publicKey.type,
clientExtensionResults: () => publicKey?.response?.clientExtensionResults || {},
getClientExtensionResults: () => publicKey?.response?.clientExtensionResults || {}
};
return Object.setPrototypeOf(publicKeyCredential, PublicKeyCredential.prototype);
};
// Posts a message to extension's content script and waits for response
const postMessageToExtension = function(request) {
return new Promise((resolve, reject) => {
const ev = document;
const listener = ((messageEvent) => {
const handler = (msg) => {
if (msg && msg.type === 'kpxc-passkeys-response' && msg.detail) {
messageEvent.removeEventListener('kpxc-passkeys-response', listener);
resolve(msg.detail);
return;
}
};
return handler;
})(ev);
ev.addEventListener('kpxc-passkeys-response', listener);
// Send the request
document.dispatchEvent(new CustomEvent('kpxc-passkeys-request', { detail: request }));
});
};
const isSameOriginWithAncestors = function() {
try {
return window.self.origin === window.top.origin;
} catch (err) {
return false;
}
};
// Throws errors to a correct exceptions
const throwError = function(errorCode, errorMessage) {
if ((!errorCode && !errorMessage) || errorCode === PASSKEYS_REQUEST_CANCELED) {
// No error or canceled by user. Stop the timer but throw no exception. Fallback with be called instead.
return;
}
if (errorCode === PASSKEYS_WAIT_FOR_LIFETIMER || errorCode === PASSKEYS_CREDENTIAL_IS_EXCLUDED) {
// Timer handled in the content script
return;
}
if ([ PASSKEYS_DOMAIN_RPID_MISMATCH, PASSKEYS_DOMAIN_IS_NOT_VALID ].includes(errorCode)) {
throw new DOMException(errorMessage, DOMException.SECURITY_ERR);
}
if (errorCode === PASSKEYS_NO_SUPPORTED_ALGORITHMS) {
throw new DOMException(errorMessage, DOMException.NOT_SUPPORTED_ERR);
}
if ([ PASSKEYS_INVALID_CHALLENGE, PASSKEYS_INVALID_USER_ID ].includes(errorCode)) {
throw new TypeError(errorMessage);
}
if (
[
PASSKEYS_ATTESTATION_NOT_SUPPORTED,
PASSKEYS_INVALID_URL_PROVIDED,
PASSKEYS_INVALID_USER_VERIFICATION,
PASSKEYS_EMPTY_PUBLIC_KEY,
PASSKEYS_UNKNOWN_ERROR,
PASSKEYS_ORIGIN_NOT_ALLOWED,
].includes(errorCode)
) {
throw new DOMException(errorMessage, 'NotAllowedError');
}
throw new DOMException(errorMessage, 'UnknownError');
};
(async () => {
const originalCredentials = navigator.credentials;
const passkeysCredentials = {
async create(options) {
if (!options.publicKey) {
return null;
}
const sameOriginWithAncestors = isSameOriginWithAncestors();
const response = await postMessageToExtension({
action: 'passkeys_create',
publicKey: options.publicKey,
sameOriginWithAncestors: sameOriginWithAncestors,
});
if (!response.publicKey) {
if (!response.fallback) {
throwError(response?.errorCode, response?.errorMessage);
}
return response.fallback ? originalCredentials.create(options) : null;
}
return createPublicKeyCredential(response.publicKey);
},
async get(options) {
if (!options.publicKey || options?.mediation === 'silent') {
return null;
}
if (options?.mediation === 'conditional') {
return originalCredentials.get(options);
}
const sameOriginWithAncestors = isSameOriginWithAncestors();
const response = await postMessageToExtension({
action: 'passkeys_get',
publicKey: options.publicKey,
sameOriginWithAncestors: sameOriginWithAncestors,
});
if (!response.publicKey) {
if (!response.fallback) {
throwError(response?.errorCode, response?.errorMessage);
}
return response.fallback ? originalCredentials.get(options) : null;
}
return createPublicKeyCredential(response.publicKey);
}
};
const isConditionalMediationAvailable = async() => false;
const isUserVerifyingPlatformAuthenticatorAvailable = async() => true;
// Overwrite navigator.credentials and PublicKeyCredential.isConditionalMediationAvailable.
// The latter requires user to select which device to use for authentication, but for now browsers cannot
// select a software authenticator. This could be removed in the future.
try {
Object.defineProperty(navigator, 'credentials', { value: passkeysCredentials });
Object.defineProperty(window.PublicKeyCredential, 'isConditionalMediationAvailable', {
value: isConditionalMediationAvailable,
});
Object.defineProperty(window.PublicKeyCredential, 'isUserVerifyingPlatformAuthenticatorAvailable', {
value: isUserVerifyingPlatformAuthenticatorAvailable,
});
} catch (err) {
console.log('Cannot override navigator.credentials: ', err);
}
})();