• 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(
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