1// Copyright (C) 2022 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {globals} from '../../../frontend/globals'; 16 17import {AdbKey} from './adb_auth'; 18 19function isPasswordCredential(cred: Credential| 20 null): cred is PasswordCredential { 21 return cred !== null && cred.type === 'password'; 22} 23 24function hasPasswordCredential() { 25 return 'PasswordCredential' in window; 26} 27 28// how long we will store the key in memory 29const KEY_IN_MEMORY_TIMEOUT = 1000 * 60 * 30; // 30 minutes 30 31// Update credential store with the given key. 32export async function maybeStoreKey(key: AdbKey): Promise<void> { 33 if (!hasPasswordCredential()) { 34 return; 35 } 36 const credential = new PasswordCredential({ 37 id: 'webusb-adb-key', 38 password: key.serializeKey(), 39 name: 'WebUSB ADB Key', 40 iconURL: `${globals.root}assets/favicon.png`, 41 }); 42 // The 'Save password?' Chrome dialogue only appears if the key is 43 // not already stored in Chrome. 44 await navigator.credentials.store(credential); 45 // 'preventSilentAccess' guarantees the user is always notified when 46 // credentials are accessed. Sometimes the user is asked to click a button 47 // and other times only a notification is shown temporarily. 48 await navigator.credentials.preventSilentAccess(); 49} 50 51export class AdbKeyManager { 52 private key?: AdbKey; 53 // Id of timer used to expire the key kept in memory. 54 private keyInMemoryTimerId?: ReturnType<typeof setTimeout>; 55 56 // Finds a key, by priority: 57 // - looking in memory (i.e. this.key) 58 // - looking in the credential store 59 // - and finally creating one from scratch if needed 60 async getKey(): Promise<AdbKey> { 61 // 1. If we have a private key in memory, we return it. 62 if (this.key) { 63 return this.key; 64 } 65 66 // 2. We try to get the private key from the browser. 67 // The mediation is set as 'optional', because we use 68 // 'preventSilentAccess', which sometimes requests the user to click 69 // on a button to allow the auth, but sometimes only shows a 70 // notification and does not require the user to click on anything. 71 // If we had set mediation to 'required', the user would have been 72 // asked to click on a button every time. 73 if (hasPasswordCredential()) { 74 const options: PasswordCredentialRequestOptions = { 75 password: true, 76 mediation: 'optional', 77 }; 78 const credential = await navigator.credentials.get(options); 79 if (isPasswordCredential(credential)) { 80 return this.assignKey(AdbKey.DeserializeKey(credential.password)); 81 } 82 } 83 84 // 3. We generate a new key pair. 85 return this.assignKey(await AdbKey.GenerateNewKeyPair()); 86 } 87 88 // Assigns the key a new value, sets a timeout for storing the key in memory 89 // and then returns the new key. 90 private assignKey(key: AdbKey): AdbKey { 91 this.key = key; 92 if (this.keyInMemoryTimerId) { 93 clearTimeout(this.keyInMemoryTimerId); 94 } 95 this.keyInMemoryTimerId = 96 setTimeout(() => this.key = undefined, KEY_IN_MEMORY_TIMEOUT); 97 return key; 98 } 99} 100