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( 20 cred: Credential | null, 21): cred is PasswordCredential { 22 return cred !== null && cred.type === 'password'; 23} 24 25function hasPasswordCredential() { 26 return 'PasswordCredential' in window; 27} 28 29// how long we will store the key in memory 30const KEY_IN_MEMORY_TIMEOUT = 1000 * 60 * 30; // 30 minutes 31 32// Update credential store with the given key. 33export async function maybeStoreKey(key: AdbKey): Promise<void> { 34 if (!hasPasswordCredential()) { 35 return; 36 } 37 const credential = new PasswordCredential({ 38 id: 'webusb-adb-key', 39 password: key.serializeKey(), 40 name: 'WebUSB ADB Key', 41 iconURL: `${globals.root}assets/favicon.png`, 42 }); 43 // The 'Save password?' Chrome dialogue only appears if the key is 44 // not already stored in Chrome. 45 await navigator.credentials.store(credential); 46 // 'preventSilentAccess' guarantees the user is always notified when 47 // credentials are accessed. Sometimes the user is asked to click a button 48 // and other times only a notification is shown temporarily. 49 await navigator.credentials.preventSilentAccess(); 50} 51 52export class AdbKeyManager { 53 private key?: AdbKey; 54 // Id of timer used to expire the key kept in memory. 55 private keyInMemoryTimerId?: ReturnType<typeof setTimeout>; 56 57 // Finds a key, by priority: 58 // - looking in memory (i.e. this.key) 59 // - looking in the credential store 60 // - and finally creating one from scratch if needed 61 async getKey(): Promise<AdbKey> { 62 // 1. If we have a private key in memory, we return it. 63 if (this.key) { 64 return this.key; 65 } 66 67 // 2. We try to get the private key from the browser. 68 // The mediation is set as 'optional', because we use 69 // 'preventSilentAccess', which sometimes requests the user to click 70 // on a button to allow the auth, but sometimes only shows a 71 // notification and does not require the user to click on anything. 72 // If we had set mediation to 'required', the user would have been 73 // asked to click on a button every time. 74 if (hasPasswordCredential()) { 75 const options: PasswordCredentialRequestOptions = { 76 password: true, 77 mediation: 'optional', 78 }; 79 const credential = await navigator.credentials.get(options); 80 if (isPasswordCredential(credential)) { 81 return this.assignKey(AdbKey.DeserializeKey(credential.password)); 82 } 83 } 84 85 // 3. We generate a new key pair. 86 return this.assignKey(await AdbKey.GenerateNewKeyPair()); 87 } 88 89 // Assigns the key a new value, sets a timeout for storing the key in memory 90 // and then returns the new key. 91 private assignKey(key: AdbKey): AdbKey { 92 this.key = key; 93 if (this.keyInMemoryTimerId) { 94 clearTimeout(this.keyInMemoryTimerId); 95 } 96 this.keyInMemoryTimerId = setTimeout( 97 () => (this.key = undefined), 98 KEY_IN_MEMORY_TIMEOUT, 99 ); 100 return key; 101 } 102} 103