|
64 | 64 | hmacSecret: false, |
65 | 65 | known: false, |
66 | 66 | }; |
| 67 | + // What the authenticator agreed to during registration, which is a different |
| 68 | + // question from what the browser can negotiate. Null until a credential |
| 69 | + // exists. A platform authenticator commonly answers yes to PRF and no to |
| 70 | + // hmac-secret on a browser that advertises both. |
| 71 | + let credentialSupport = null; |
67 | 72 | let useWorkerKeystore = false; |
68 | 73 | let workerAvailable = false; |
69 | 74 | let workerClient = null; |
|
148 | 153 | }; |
149 | 154 | } |
150 | 155 |
|
151 | | - await initializeWebAuthn(); |
| 156 | + // Client capabilities first, then the credential. The order matters: |
| 157 | + // initializeWebAuthn() loads a stored credential and lets its recorded |
| 158 | + // answer refine the choice, and checkEncryptionSupport() picks a method |
| 159 | + // from the browser's view alone — running it second would undo that. |
152 | 160 | await checkEncryptionSupport(); |
| 161 | + await initializeWebAuthn(); |
153 | 162 | }); |
154 | 163 |
|
155 | 164 | onDestroy(() => { |
156 | 165 | resetWorkerClient(); |
157 | 166 | }); |
158 | 167 |
|
159 | | - // Three states, not two: an extension we know is missing reads differently |
160 | | - // from one the browser would not tell us about. |
161 | | - function supportLabel(flag) { |
162 | | - if (flag) return '✅ Supported'; |
| 168 | + // The method values and the support-object keys do not spell the extension |
| 169 | + // the same way; keep the translation in one place rather than at each use. |
| 170 | + const SUPPORT_KEY = { |
| 171 | + prf: 'prf', |
| 172 | + largeBlob: 'largeBlob', |
| 173 | + 'hmac-secret': 'hmacSecret', |
| 174 | + }; |
| 175 | + const METHOD_PREFERENCE = ['prf', 'largeBlob', 'hmac-secret']; |
| 176 | +
|
| 177 | + // Four states. Once a credential exists its answer overrules the browser's: |
| 178 | + // the browser saying yes only means it would pass the request along. |
| 179 | + function supportLabel(name) { |
| 180 | + if (credentialSupport) { |
| 181 | + if (credentialSupport[name]) return '✅ Supported'; |
| 182 | + if (extensionSupport[name]) return '⚠️ Browser yes, this passkey no'; |
| 183 | + return '❌ Not supported'; |
| 184 | + } |
| 185 | + if (extensionSupport[name]) return '✅ Supported'; |
163 | 186 | return extensionSupport.known ? '❌ Not supported' : '❓ Unknown'; |
164 | 187 | } |
165 | 188 |
|
| 189 | + // May the method be offered at all? Before a credential exists we go by the |
| 190 | + // browser and keep anything it cannot vouch for selectable. Afterwards the |
| 191 | + // authenticator decides, because it is the one that has to deliver. |
| 192 | + function methodAvailable(method) { |
| 193 | + const name = SUPPORT_KEY[method]; |
| 194 | + if (credentialSupport) return credentialSupport[name] === true; |
| 195 | + return !extensionSupport.known || extensionSupport[name] === true; |
| 196 | + } |
| 197 | +
|
| 198 | + /** |
| 199 | + * Adopt what the authenticator agreed to, and step off a method it refused. |
| 200 | + * |
| 201 | + * Without this the UI keeps promising a method the ceremony will not honour — |
| 202 | + * which is how "hmac-secret ✅ Supported" ends in "No hmac-secret output from |
| 203 | + * credential" (issue #9). |
| 204 | + */ |
| 205 | + function applyCredentialSupport(support) { |
| 206 | + if (!support) return; |
| 207 | + credentialSupport = support; |
| 208 | + console.log('Authenticator extension support:', support); |
| 209 | +
|
| 210 | + if (methodAvailable(encryptionMethod)) return; |
| 211 | +
|
| 212 | + const fallback = METHOD_PREFERENCE.find((method) => |
| 213 | + methodAvailable(method) |
| 214 | + ); |
| 215 | +
|
| 216 | + if (fallback) { |
| 217 | + console.warn( |
| 218 | + `Authenticator does not support ${encryptionMethod}; falling back to ${fallback}` |
| 219 | + ); |
| 220 | + encryptionMethod = fallback; |
| 221 | + } else { |
| 222 | + console.warn( |
| 223 | + 'Authenticator supports none of the encryption extensions; keystore encryption disabled' |
| 224 | + ); |
| 225 | + useEncryption = false; |
| 226 | + } |
| 227 | + } |
| 228 | +
|
166 | 229 | async function checkEncryptionSupport() { |
167 | 230 | try { |
168 | 231 | extensionSupport = await KeystoreEncryption.checkExtensionSupport(); |
|
211 | 274 | // Load stored credential |
212 | 275 | credential = loadStoredCredential(); |
213 | 276 | if (credential) { |
| 277 | + // Credentials registered before this was recorded carry no answer, so |
| 278 | + // the browser's view stands until the next registration. |
| 279 | + applyCredentialSupport(credential.extensionSupport); |
214 | 280 | status = 'Credential found, ready to authenticate!'; |
215 | 281 | } |
216 | 282 | } catch (error) { |
|
376 | 442 | keystoreEncryptionMethod: encryptionMethod, |
377 | 443 | }); |
378 | 444 |
|
| 445 | + // The ceremony has now answered what the browser could only guess at. |
| 446 | + applyCredentialSupport(credential.extensionSupport); |
| 447 | +
|
379 | 448 | // Store credential for future use |
380 | 449 | storeCredential(credential); |
381 | 450 |
|
|
935 | 1004 | type="radio" |
936 | 1005 | bind:group={encryptionMethod} |
937 | 1006 | value="prf" |
938 | | - disabled={loading || |
939 | | - (extensionSupport.known && !extensionSupport.prf)} |
| 1007 | + disabled={loading || !methodAvailable('prf')} |
940 | 1008 | style="cursor: pointer;" |
941 | 1009 | /> |
942 | 1010 | <span style="color: var(--cds-text-primary);">PRF</span> |
|
953 | 1021 | type="radio" |
954 | 1022 | bind:group={encryptionMethod} |
955 | 1023 | value="largeBlob" |
956 | | - disabled={loading || |
957 | | - (extensionSupport.known && !extensionSupport.largeBlob)} |
| 1024 | + disabled={loading || !methodAvailable('largeBlob')} |
958 | 1025 | style="cursor: pointer;" |
959 | 1026 | /> |
960 | 1027 | <span style="color: var(--cds-text-primary);">largeBlob</span> |
|
971 | 1038 | type="radio" |
972 | 1039 | bind:group={encryptionMethod} |
973 | 1040 | value="hmac-secret" |
974 | | - disabled={loading || |
975 | | - (extensionSupport.known && !extensionSupport.hmacSecret)} |
| 1041 | + disabled={loading || !methodAvailable('hmac-secret')} |
976 | 1042 | style="cursor: pointer;" |
977 | 1043 | /> |
978 | 1044 | <span style="color: var(--cds-text-primary);" |
|
0 commit comments