• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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