| // Copyright (C) 2022 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {globals} from '../../../frontend/globals'; |
| |
| import {AdbKey} from './adb_auth'; |
| |
| function isPasswordCredential(cred: Credential| |
| null): cred is PasswordCredential { |
| return cred !== null && cred.type === 'password'; |
| } |
| |
| function hasPasswordCredential() { |
| return 'PasswordCredential' in window; |
| } |
| |
| // how long we will store the key in memory |
| const KEY_IN_MEMORY_TIMEOUT = 1000 * 60 * 30; // 30 minutes |
| |
| // Update credential store with the given key. |
| export async function maybeStoreKey(key: AdbKey): Promise<void> { |
| if (!hasPasswordCredential()) { |
| return; |
| } |
| const credential = new PasswordCredential({ |
| id: 'webusb-adb-key', |
| password: key.serializeKey(), |
| name: 'WebUSB ADB Key', |
| iconURL: `${globals.root}assets/favicon.png`, |
| }); |
| // The 'Save password?' Chrome dialogue only appears if the key is |
| // not already stored in Chrome. |
| await navigator.credentials.store(credential); |
| // 'preventSilentAccess' guarantees the user is always notified when |
| // credentials are accessed. Sometimes the user is asked to click a button |
| // and other times only a notification is shown temporarily. |
| await navigator.credentials.preventSilentAccess(); |
| } |
| |
| export class AdbKeyManager { |
| private key?: AdbKey; |
| // Id of timer used to expire the key kept in memory. |
| private keyInMemoryTimerId?: ReturnType<typeof setTimeout>; |
| |
| // Finds a key, by priority: |
| // - looking in memory (i.e. this.key) |
| // - looking in the credential store |
| // - and finally creating one from scratch if needed |
| async getKey(): Promise<AdbKey> { |
| // 1. If we have a private key in memory, we return it. |
| if (this.key) { |
| return this.key; |
| } |
| |
| // 2. We try to get the private key from the browser. |
| // The mediation is set as 'optional', because we use |
| // 'preventSilentAccess', which sometimes requests the user to click |
| // on a button to allow the auth, but sometimes only shows a |
| // notification and does not require the user to click on anything. |
| // If we had set mediation to 'required', the user would have been |
| // asked to click on a button every time. |
| if (hasPasswordCredential()) { |
| const options: PasswordCredentialRequestOptions = { |
| password: true, |
| mediation: 'optional', |
| }; |
| const credential = await navigator.credentials.get(options); |
| if (isPasswordCredential(credential)) { |
| return this.assignKey(AdbKey.DeserializeKey(credential.password)); |
| } |
| } |
| |
| // 3. We generate a new key pair. |
| return this.assignKey(await AdbKey.GenerateNewKeyPair()); |
| } |
| |
| // Assigns the key a new value, sets a timeout for storing the key in memory |
| // and then returns the new key. |
| private assignKey(key: AdbKey): AdbKey { |
| this.key = key; |
| if (this.keyInMemoryTimerId) { |
| clearTimeout(this.keyInMemoryTimerId); |
| } |
| this.keyInMemoryTimerId = |
| setTimeout(() => this.key = undefined, KEY_IN_MEMORY_TIMEOUT); |
| return key; |
| } |
| } |