blob: 73575ead2fbc906495bfe12c49d62ab2fa87cb9c [file] [log] [blame]
// 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;
}
}