| // 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 {BigInteger, RSAKey} from 'jsbn-rsa'; |
| |
| import {assertExists, assertTrue} from '../../../base/logging'; |
| import { |
| base64Decode, |
| base64Encode, |
| hexEncode, |
| } from '../../../base/string_utils'; |
| import {RecordingError} from '../recording_error_handling'; |
| |
| const WORD_SIZE = 4; |
| const MODULUS_SIZE_BITS = 2048; |
| const MODULUS_SIZE = MODULUS_SIZE_BITS / 8; |
| const MODULUS_SIZE_WORDS = MODULUS_SIZE / WORD_SIZE; |
| const PUBKEY_ENCODED_SIZE = 3 * WORD_SIZE + 2 * MODULUS_SIZE; |
| const ADB_WEB_CRYPTO_ALGORITHM = { |
| name: 'RSASSA-PKCS1-v1_5', |
| hash: {name: 'SHA-1'}, |
| publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 |
| modulusLength: MODULUS_SIZE_BITS, |
| }; |
| |
| const ADB_WEB_CRYPTO_EXPORTABLE = true; |
| const ADB_WEB_CRYPTO_OPERATIONS: KeyUsage[] = ['sign']; |
| |
| const SIGNING_ASN1_PREFIX = [ |
| 0x00, |
| 0x30, |
| 0x21, |
| 0x30, |
| 0x09, |
| 0x06, |
| 0x05, |
| 0x2B, |
| 0x0E, |
| 0x03, |
| 0x02, |
| 0x1A, |
| 0x05, |
| 0x00, |
| 0x04, |
| 0x14, |
| ]; |
| |
| const R32 = BigInteger.ONE.shiftLeft(32); // 1 << 32 |
| |
| interface ValidJsonWebKey { |
| n: string; |
| e: string; |
| d: string; |
| p: string; |
| q: string; |
| dp: string; |
| dq: string; |
| qi: string; |
| } |
| |
| function isValidJsonWebKey(key: JsonWebKey): key is ValidJsonWebKey { |
| return key.n !== undefined && key.e !== undefined && key.d !== undefined && |
| key.p !== undefined && key.q !== undefined && key.dp !== undefined && |
| key.dq !== undefined && key.qi !== undefined; |
| } |
| |
| // Convert a BigInteger to an array of a specified size in bytes. |
| function bigIntToFixedByteArray(bn: BigInteger, size: number): Uint8Array { |
| const paddedBnBytes = bn.toByteArray(); |
| let firstNonZeroIndex = 0; |
| while (firstNonZeroIndex < paddedBnBytes.length && |
| paddedBnBytes[firstNonZeroIndex] === 0) { |
| firstNonZeroIndex++; |
| } |
| const bnBytes = Uint8Array.from(paddedBnBytes.slice(firstNonZeroIndex)); |
| const res = new Uint8Array(size); |
| assertTrue(bnBytes.length <= res.length); |
| res.set(bnBytes, res.length - bnBytes.length); |
| return res; |
| } |
| |
| export class AdbKey { |
| // We use this JsonWebKey to: |
| // - create a private key and sign with it |
| // - create a public key and send it to the device |
| // - serialize the JsonWebKey and send it to the device (or retrieve it |
| // from the device and deserialize) |
| jwkPrivate: ValidJsonWebKey; |
| |
| private constructor(jwkPrivate: ValidJsonWebKey) { |
| this.jwkPrivate = jwkPrivate; |
| } |
| |
| static async GenerateNewKeyPair(): Promise<AdbKey> { |
| // Construct a new CryptoKeyPair and keep its private key in JWB format. |
| const keyPair = await crypto.subtle.generateKey( |
| ADB_WEB_CRYPTO_ALGORITHM, |
| ADB_WEB_CRYPTO_EXPORTABLE, |
| ADB_WEB_CRYPTO_OPERATIONS); |
| const jwkPrivate = await crypto.subtle.exportKey('jwk', keyPair.privateKey); |
| if (!isValidJsonWebKey(jwkPrivate)) { |
| throw new RecordingError('Could not generate a valid private key.'); |
| } |
| return new AdbKey(jwkPrivate); |
| } |
| |
| static DeserializeKey(serializedKey: string): AdbKey { |
| return new AdbKey(JSON.parse(serializedKey)); |
| } |
| |
| // Perform an RSA signing operation for the ADB auth challenge. |
| // |
| // For the RSA signature, the token is expected to have already |
| // had the SHA-1 message digest applied. |
| // |
| // However, the adb token we receive from the device is made up of 20 randomly |
| // generated bytes that are treated like a SHA-1. Therefore, we need to update |
| // the message format. |
| sign(token: Uint8Array): Uint8Array { |
| const rsaKey = new RSAKey(); |
| rsaKey.setPrivateEx( |
| hexEncode(base64Decode(this.jwkPrivate.n)), |
| hexEncode(base64Decode(this.jwkPrivate.e)), |
| hexEncode(base64Decode(this.jwkPrivate.d)), |
| hexEncode(base64Decode(this.jwkPrivate.p)), |
| hexEncode(base64Decode(this.jwkPrivate.q)), |
| hexEncode(base64Decode(this.jwkPrivate.dp)), |
| hexEncode(base64Decode(this.jwkPrivate.dq)), |
| hexEncode(base64Decode(this.jwkPrivate.qi))); |
| assertTrue(rsaKey.n.bitLength() === MODULUS_SIZE_BITS); |
| |
| // Message Layout (size equals that of the key modulus): |
| // 00 01 FF FF FF FF ... FF [ASN.1 PREFIX] [TOKEN] |
| const message = new Uint8Array(MODULUS_SIZE); |
| |
| // Initially fill the buffer with the padding |
| message.fill(0xFF); |
| |
| // add prefix |
| message[0] = 0x00; |
| message[1] = 0x01; |
| |
| // add the ASN.1 prefix |
| message.set( |
| SIGNING_ASN1_PREFIX, |
| message.length - SIGNING_ASN1_PREFIX.length - token.length); |
| |
| // then the actual token at the end |
| message.set(token, message.length - token.length); |
| |
| const messageInteger = new BigInteger(Array.from(message)); |
| const signature = rsaKey.doPrivate(messageInteger); |
| return new Uint8Array(bigIntToFixedByteArray(signature, MODULUS_SIZE)); |
| } |
| |
| // Construct public key to match the adb format: |
| // go/codesearch/rvc-arc/system/core/libcrypto_utils/android_pubkey.c;l=38-53 |
| getPublicKey(): string { |
| const rsaKey = new RSAKey(); |
| rsaKey.setPublic( |
| hexEncode(base64Decode((assertExists(this.jwkPrivate.n)))), |
| hexEncode(base64Decode((assertExists(this.jwkPrivate.e))))); |
| |
| const n0inv = R32.subtract(rsaKey.n.modInverse(R32)).intValue(); |
| const r = BigInteger.ONE.shiftLeft(1).pow(MODULUS_SIZE_BITS); |
| const rr = r.multiply(r).mod(rsaKey.n); |
| |
| const buffer = new ArrayBuffer(PUBKEY_ENCODED_SIZE); |
| const dv = new DataView(buffer); |
| dv.setUint32(0, MODULUS_SIZE_WORDS, true); |
| dv.setUint32(WORD_SIZE, n0inv, true); |
| |
| const dvU8 = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength); |
| dvU8.set( |
| bigIntToFixedByteArray(rsaKey.n, MODULUS_SIZE).reverse(), |
| 2 * WORD_SIZE); |
| dvU8.set( |
| bigIntToFixedByteArray(rr, MODULUS_SIZE).reverse(), |
| 2 * WORD_SIZE + MODULUS_SIZE); |
| |
| dv.setUint32(2 * WORD_SIZE + 2 * MODULUS_SIZE, rsaKey.e, true); |
| return base64Encode(dvU8) + ' ui.perfetto.dev'; |
| } |
| |
| serializeKey(): string { |
| return JSON.stringify(this.jwkPrivate); |
| } |
| } |