appium-adb 14.0.6 → 14.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import _ from 'lodash';
2
2
  import _fs from 'fs';
3
- import { exec } from 'teen_process';
3
+ import { exec, type ExecError } from 'teen_process';
4
4
  import path from 'path';
5
5
  import { log } from '../logger.js';
6
6
  import { tempDir, system, mkdirp, fs, util, zip } from '@appium/support';
@@ -11,6 +11,13 @@ import {
11
11
  APKS_EXTENSION,
12
12
  getResourcePath,
13
13
  } from '../helpers.js';
14
+ import type { ADB } from '../adb.js';
15
+ import type {
16
+ StringRecord,
17
+ SignedAppCacheValue,
18
+ CertCheckOptions,
19
+ KeystoreHash
20
+ } from './types.js';
14
21
 
15
22
  const DEFAULT_PRIVATE_KEY = path.join('keys', 'testkey.pk8');
16
23
  const DEFAULT_CERTIFICATE = path.join('keys', 'testkey.x509.pem');
@@ -20,25 +27,23 @@ const SHA1 = 'sha1';
20
27
  const SHA256 = 'sha256';
21
28
  const SHA512 = 'sha512';
22
29
  const MD5 = 'md5';
23
- const DEFAULT_CERT_HASH = {
30
+ const DEFAULT_CERT_HASH: KeystoreHash = {
24
31
  [SHA256]: 'a40da80a59d170caa950cf15c18c454d47a39b26989d8b640ecd745ba71bf5dc'
25
32
  };
26
33
  const JAVA_PROPS_INIT_ERROR = 'java.lang.Error: Properties init';
27
- /** @type {LRUCache<string, import('./types').SignedAppCacheValue>} */
28
- const SIGNED_APPS_CACHE = new LRUCache({
34
+ const SIGNED_APPS_CACHE = new LRUCache<string, SignedAppCacheValue>({
29
35
  max: 30,
30
36
  });
31
37
 
32
38
  /**
33
39
  * Execute apksigner utility with given arguments.
34
40
  *
35
- * @this {import('../adb.js').ADB}
36
- * @param {string[]} args - The list of tool arguments.
37
- * @return {Promise<string>} - Command stdout
38
- * @throws {Error} If apksigner binary is not present on the local file system
41
+ * @param args - The list of tool arguments.
42
+ * @returns - Command stdout
43
+ * @throws If apksigner binary is not present on the local file system
39
44
  * or the return code is not equal to zero.
40
45
  */
41
- export async function executeApksigner (args) {
46
+ export async function executeApksigner (this: ADB, args: string[]): Promise<string> {
42
47
  const apkSignerJar = await getApksignerForOs.bind(this)();
43
48
  const fullCmd = [
44
49
  await getJavaForOs(), '-Xmx1024M', '-Xss1m',
@@ -52,18 +57,20 @@ export async function executeApksigner (args) {
52
57
  // @ts-ignore This works
53
58
  windowsVerbatimArguments: system.isWindows(),
54
59
  });
55
- for (let [name, stream] of [['stdout', stdout], ['stderr', stderr]]) {
60
+ for (const [name, stream] of [['stdout', stdout], ['stderr', stderr]] as const) {
56
61
  if (!_.trim(stream)) {
57
62
  continue;
58
63
  }
59
64
 
60
65
  if (name === 'stdout') {
61
66
  // Make the output less talkative
62
- stream = stream.split('\n')
67
+ const filteredStream = stream.split('\n')
63
68
  .filter((line) => !line.includes('WARNING:'))
64
69
  .join('\n');
70
+ log.debug(`apksigner ${name}: ${filteredStream}`);
71
+ } else {
72
+ log.debug(`apksigner ${name}: ${stream}`);
65
73
  }
66
- log.debug(`apksigner ${name}: ${stream}`);
67
74
  }
68
75
  return stdout;
69
76
  }
@@ -71,11 +78,10 @@ export async function executeApksigner (args) {
71
78
  /**
72
79
  * (Re)sign the given apk file on the local file system with the default certificate.
73
80
  *
74
- * @this {import('../adb.js').ADB}
75
- * @param {string} apk - The full path to the local apk file.
76
- * @throws {Error} If signing fails.
81
+ * @param apk - The full path to the local apk file.
82
+ * @throws If signing fails.
77
83
  */
78
- export async function signWithDefaultCert (apk) {
84
+ export async function signWithDefaultCert (this: ADB, apk: string): Promise<void> {
79
85
  log.debug(`Signing '${apk}' with default cert`);
80
86
  if (!(await fs.exists(apk))) {
81
87
  throw new Error(`${apk} file doesn't exist.`);
@@ -90,21 +96,21 @@ export async function signWithDefaultCert (apk) {
90
96
  try {
91
97
  await this.executeApksigner(args);
92
98
  } catch (e) {
99
+ const err = e as ExecError;
93
100
  throw new Error(`Could not sign '${apk}' with the default certificate. ` +
94
- `Original error: ${e.stderr || e.stdout || e.message}`);
101
+ `Original error: ${err.stderr || err.stdout || err.message}`);
95
102
  }
96
103
  }
97
104
 
98
105
  /**
99
106
  * (Re)sign the given apk file on the local file system with a custom certificate.
100
107
  *
101
- * @this {import('../adb.js').ADB}
102
- * @param {string} apk - The full path to the local apk file.
103
- * @throws {Error} If signing fails.
108
+ * @param apk - The full path to the local apk file.
109
+ * @throws If signing fails.
104
110
  */
105
- export async function signWithCustomCert (apk) {
111
+ export async function signWithCustomCert (this: ADB, apk: string): Promise<void> {
106
112
  log.debug(`Signing '${apk}' with custom cert`);
107
- if (!(await fs.exists(/** @type {string} */(this.keystorePath)))) {
113
+ if (!(await fs.exists(this.keystorePath as string))) {
108
114
  throw new Error(`Keystore: ${this.keystorePath} doesn't exist.`);
109
115
  }
110
116
  if (!(await fs.exists(apk))) {
@@ -113,14 +119,15 @@ export async function signWithCustomCert (apk) {
113
119
 
114
120
  try {
115
121
  await this.executeApksigner(['sign',
116
- '--ks', /** @type {string} */(this.keystorePath),
117
- '--ks-key-alias', /** @type {string} */(this.keyAlias),
122
+ '--ks', this.keystorePath as string,
123
+ '--ks-key-alias', this.keyAlias as string,
118
124
  '--ks-pass', `pass:${this.keystorePassword}`,
119
125
  '--key-pass', `pass:${this.keyPassword}`,
120
126
  apk]);
121
127
  } catch (err) {
128
+ const error = err as ExecError;
122
129
  log.warn(`Cannot use apksigner tool for signing. Defaulting to jarsigner. ` +
123
- `Original error: ${err.stderr || err.stdout || err.message}`);
130
+ `Original error: ${error.stderr || error.stdout || error.message}`);
124
131
  try {
125
132
  if (await unsignApk(apk)) {
126
133
  log.debug(`'${apk}' has been successfully unsigned`);
@@ -129,22 +136,22 @@ export async function signWithCustomCert (apk) {
129
136
  }
130
137
  const jarsigner = path.resolve(await getJavaHome(), 'bin',
131
138
  `jarsigner${system.isWindows() ? '.exe' : ''}`);
132
- /** @type {string[]} */
133
- const fullCmd = [jarsigner,
139
+ const fullCmd: string[] = [jarsigner,
134
140
  '-sigalg', 'MD5withRSA',
135
141
  '-digestalg', 'SHA1',
136
- '-keystore', /** @type {string} */(this.keystorePath),
137
- '-storepass', /** @type {string} */(this.keystorePassword),
138
- '-keypass', /** @type {string} */(this.keyPassword),
139
- apk, /** @type {string} */(this.keyAlias)];
142
+ '-keystore', this.keystorePath as string,
143
+ '-storepass', this.keystorePassword as string,
144
+ '-keypass', this.keyPassword as string,
145
+ apk, this.keyAlias as string];
140
146
  log.debug(`Starting jarsigner: ${util.quote(fullCmd)}`);
141
147
  await exec(fullCmd[0], fullCmd.slice(1), {
142
148
  // @ts-ignore This works
143
149
  windowsVerbatimArguments: system.isWindows(),
144
150
  });
145
151
  } catch (e) {
152
+ const execErr = e as ExecError;
146
153
  throw new Error(`Could not sign with custom certificate. ` +
147
- `Original error: ${e.stderr || e.message}`);
154
+ `Original error: ${execErr.stderr || execErr.message}`);
148
155
  }
149
156
  }
150
157
  }
@@ -154,11 +161,10 @@ export async function signWithCustomCert (apk) {
154
161
  * custom or default certificate based on _this.useKeystore_ property value
155
162
  * and Zip-aligns it after signing.
156
163
  *
157
- * @this {import('../adb.js').ADB}
158
- * @param {string} appPath - The full path to the local .apk(s) file.
159
- * @throws {Error} If signing fails.
164
+ * @param appPath - The full path to the local .apk(s) file.
165
+ * @throws If signing fails.
160
166
  */
161
- export async function sign (appPath) {
167
+ export async function sign (this: ADB, appPath: string): Promise<void> {
162
168
  if (appPath.endsWith(APKS_EXTENSION)) {
163
169
  let message = 'Signing of .apks-files is not supported. ';
164
170
  if (this.useKeystore) {
@@ -186,16 +192,15 @@ export async function sign (appPath) {
186
192
  /**
187
193
  * Perform zip-aligning to the given local apk file.
188
194
  *
189
- * @this {import('../adb.js').ADB}
190
- * @param {string} apk - The full path to the local apk file.
191
- * @returns {Promise<boolean>} True if the apk has been successfully aligned
195
+ * @param apk - The full path to the local apk file.
196
+ * @returns True if the apk has been successfully aligned
192
197
  * or false if the apk has been already aligned.
193
- * @throws {Error} If zip-align fails.
198
+ * @throws If zip-align fails.
194
199
  */
195
- export async function zipAlignApk (apk) {
200
+ export async function zipAlignApk (this: ADB, apk: string): Promise<boolean> {
196
201
  await this.initZipAlign();
197
202
  try {
198
- await exec((/** @type {import('./types').StringRecord} */ (this.binaries)).zipalign, ['-c', '4', apk]);
203
+ await exec((this.binaries as StringRecord).zipalign as string, ['-c', '4', apk]);
199
204
  log.debug(`${apk}' is already zip-aligned. Doing nothing`);
200
205
  return false;
201
206
  } catch {
@@ -212,43 +217,44 @@ export async function zipAlignApk (apk) {
212
217
  await mkdirp(path.dirname(alignedApk));
213
218
  try {
214
219
  await exec(
215
- (/** @type {import('./types').StringRecord} */ (this.binaries)).zipalign,
220
+ (this.binaries as StringRecord).zipalign as string,
216
221
  ['-f', '4', apk, alignedApk]
217
222
  );
218
223
  await fs.mv(alignedApk, apk, { mkdirp: true });
219
224
  return true;
220
225
  } catch (e) {
226
+ const err = e as Error;
221
227
  if (await fs.exists(alignedApk)) {
222
228
  await fs.unlink(alignedApk);
223
229
  }
224
- throw new Error(`zipAlignApk failed. Original error: ${e.stderr || e.message}`);
230
+ throw new Error(`zipAlignApk failed. Original error: ${err.message || (err as ExecError).stderr}`);
225
231
  }
226
232
  }
227
233
 
228
234
  /**
229
235
  * Check if the app is already signed with the default Appium certificate.
230
236
  *
231
- * @this {import('../adb.js').ADB}
232
- * @param {string} appPath - The full path to the local .apk(s) file.
233
- * @param {string} pkg - The name of application package.
234
- * @param {import('./types').CertCheckOptions} [opts={}] - Certificate checking options
235
- * @return {Promise<boolean>} True if given application is already signed.
237
+ * @param appPath - The full path to the local .apk(s) file.
238
+ * @param pkg - The name of application package.
239
+ * @param opts - Certificate checking options
240
+ * @returns True if given application is already signed.
236
241
  */
237
- export async function checkApkCert (appPath, pkg, opts = {}) {
242
+ export async function checkApkCert (this: ADB, appPath: string, pkg: string, opts: CertCheckOptions = {}): Promise<boolean> {
238
243
  log.debug(`Checking app cert for ${appPath}`);
239
244
  if (!await fs.exists(appPath)) {
240
245
  log.debug(`'${appPath}' does not exist`);
241
246
  return false;
242
247
  }
243
248
 
249
+ let actualAppPath = appPath;
244
250
  if (path.extname(appPath) === APKS_EXTENSION) {
245
- appPath = await this.extractBaseApk(appPath);
251
+ actualAppPath = await this.extractBaseApk(appPath);
246
252
  }
247
253
 
248
- const hashMatches = (apksignerOutput, expectedHashes) => {
254
+ const hashMatches = (apksignerOutput: string, expectedHashes: KeystoreHash): boolean => {
249
255
  for (const [name, value] of _.toPairs(expectedHashes)) {
250
- if (new RegExp(`digest:\\s+${value}\\b`, 'i').test(apksignerOutput)) {
251
- log.debug(`${name} hash did match for '${path.basename(appPath)}'`);
256
+ if (value && new RegExp(`digest:\\s+${value}\\b`, 'i').test(apksignerOutput)) {
257
+ log.debug(`${name} hash did match for '${path.basename(actualAppPath)}'`);
252
258
  return true;
253
259
  }
254
260
  }
@@ -259,27 +265,28 @@ export async function checkApkCert (appPath, pkg, opts = {}) {
259
265
  requireDefaultCert = true,
260
266
  } = opts;
261
267
 
262
- const appHash = await fs.hash(appPath);
268
+ const appHash = await fs.hash(actualAppPath);
263
269
  if (SIGNED_APPS_CACHE.has(appHash)) {
264
- log.debug(`Using the previously cached signature entry for '${path.basename(appPath)}'`);
265
- const {keystorePath, output, expected} = /** @type {import('./types').SignedAppCacheValue} */ (
266
- SIGNED_APPS_CACHE.get(appHash)
267
- );
268
- if (this.useKeystore && this.keystorePath === keystorePath || !this.useKeystore) {
269
- return (!this.useKeystore && !requireDefaultCert) || hashMatches(output, expected);
270
+ log.debug(`Using the previously cached signature entry for '${path.basename(actualAppPath)}'`);
271
+ const cached = SIGNED_APPS_CACHE.get(appHash);
272
+ if (cached) {
273
+ const {keystorePath, output, expected} = cached;
274
+ if (this.useKeystore && this.keystorePath === keystorePath || !this.useKeystore) {
275
+ return (!this.useKeystore && !requireDefaultCert) || hashMatches(output, expected);
276
+ }
270
277
  }
271
278
  }
272
279
 
273
280
  const expected = this.useKeystore ? await this.getKeystoreHash() : DEFAULT_CERT_HASH;
274
281
  try {
275
282
  await getApksignerForOs.bind(this)();
276
- const output = await this.executeApksigner(['verify', '--print-certs', appPath]);
283
+ const output = await this.executeApksigner(['verify', '--print-certs', actualAppPath]);
277
284
  const hasMatch = hashMatches(output, expected);
278
285
  if (hasMatch) {
279
- log.info(`'${appPath}' is signed with the ` +
286
+ log.info(`'${actualAppPath}' is signed with the ` +
280
287
  `${this.useKeystore ? 'keystore' : 'default'} certificate`);
281
288
  } else {
282
- log.info(`'${appPath}' is signed with a ` +
289
+ log.info(`'${actualAppPath}' is signed with a ` +
283
290
  `non-${this.useKeystore ? 'keystore' : 'default'} certificate`);
284
291
  }
285
292
  const isSigned = (!this.useKeystore && !requireDefaultCert) || hasMatch;
@@ -287,17 +294,18 @@ export async function checkApkCert (appPath, pkg, opts = {}) {
287
294
  SIGNED_APPS_CACHE.set(appHash, {
288
295
  output,
289
296
  expected,
290
- keystorePath: /** @type {string} */ (this.keystorePath),
297
+ keystorePath: this.keystorePath as string,
291
298
  });
292
299
  }
293
300
  return isSigned;
294
301
  } catch (err) {
302
+ const error = err as ExecError;
295
303
  // check if there is no signature
296
- if (_.includes(err.stderr, APKSIGNER_VERIFY_FAIL)) {
297
- log.info(`'${appPath}' is not signed`);
304
+ if (_.includes(error.stderr, APKSIGNER_VERIFY_FAIL)) {
305
+ log.info(`'${actualAppPath}' is not signed`);
298
306
  return false;
299
307
  }
300
- const errMsg = err.stderr || err.stdout || err.message;
308
+ const errMsg = error.stderr || error.stdout || error.message;
301
309
  if (_.includes(errMsg, JAVA_PROPS_INIT_ERROR)) {
302
310
  // This error pops up randomly and we are not quite sure why.
303
311
  // My guess - a race condition in java vm initialization.
@@ -308,10 +316,10 @@ export async function checkApkCert (appPath, pkg, opts = {}) {
308
316
  // would anyway fail.
309
317
  // See https://github.com/appium/appium/issues/14724 for more details.
310
318
  log.warn(errMsg);
311
- log.warn(`Assuming '${appPath}' is already signed and continuing anyway`);
319
+ log.warn(`Assuming '${actualAppPath}' is already signed and continuing anyway`);
312
320
  return true;
313
321
  }
314
- throw new Error(`Cannot verify the signature of '${appPath}'. ` +
322
+ throw new Error(`Cannot verify the signature of '${actualAppPath}'. ` +
315
323
  `Original error: ${errMsg}`);
316
324
  }
317
325
  }
@@ -319,23 +327,21 @@ export async function checkApkCert (appPath, pkg, opts = {}) {
319
327
  /**
320
328
  * Retrieve the the hash of the given keystore.
321
329
  *
322
- * @this {import('../adb.js').ADB}
323
- * @return {Promise<import('./types').KeystoreHash>}
324
- * @throws {Error} If getting keystore hash fails.
330
+ * @returns
331
+ * @throws If getting keystore hash fails.
325
332
  */
326
- export async function getKeystoreHash () {
333
+ export async function getKeystoreHash (this: ADB): Promise<KeystoreHash> {
327
334
  log.debug(`Getting hash of the '${this.keystorePath}' keystore`);
328
335
  const keytool = path.resolve(await getJavaHome(), 'bin',
329
336
  `keytool${system.isWindows() ? '.exe' : ''}`);
330
337
  if (!await fs.exists(keytool)) {
331
338
  throw new Error(`The keytool utility cannot be found at '${keytool}'`);
332
339
  }
333
- /** @type {string[]} */
334
- const args = [
340
+ const args: string[] = [
335
341
  '-v', '-list',
336
- '-alias', /** @type {string} */ (this.keyAlias),
337
- '-keystore', /** @type {string} */ (this.keystorePath),
338
- '-storepass', /** @type {string} */ (this.keystorePassword),
342
+ '-alias', this.keyAlias as string,
343
+ '-keystore', this.keystorePath as string,
344
+ '-storepass', this.keystorePassword as string,
339
345
  ];
340
346
  log.info(`Running '${keytool}' with arguments: ${util.quote(args)}`);
341
347
  try {
@@ -343,7 +349,7 @@ export async function getKeystoreHash () {
343
349
  // @ts-ignore This property is ok
344
350
  windowsVerbatimArguments: system.isWindows(),
345
351
  });
346
- const result = {};
352
+ const result: KeystoreHash = {};
347
353
  for (const hashName of [SHA512, SHA256, SHA1, MD5]) {
348
354
  const hashRe = new RegExp(`^\\s*${hashName}:\\s*([a-f0-9:]+)`, 'mi');
349
355
  const match = hashRe.exec(stdout);
@@ -359,8 +365,9 @@ export async function getKeystoreHash () {
359
365
  log.debug(`Keystore hash: ${JSON.stringify(result)}`);
360
366
  return result;
361
367
  } catch (e) {
368
+ const err = e as ExecError;
362
369
  throw new Error(`Cannot get the hash of '${this.keystorePath}' keystore. ` +
363
- `Original error: ${e.stderr || e.message}`);
370
+ `Original error: ${err.stderr || err.message}`);
364
371
  }
365
372
  }
366
373
 
@@ -369,11 +376,10 @@ export async function getKeystoreHash () {
369
376
  /**
370
377
  * Get the absolute path to apksigner tool
371
378
  *
372
- * @this {import('../adb').ADB}
373
- * @returns {Promise<string>} An absolute path to apksigner tool.
374
- * @throws {Error} If the tool is not present on the local file system.
379
+ * @returns An absolute path to apksigner tool.
380
+ * @throws If the tool is not present on the local file system.
375
381
  */
376
- export async function getApksignerForOs () {
382
+ export async function getApksignerForOs (this: ADB): Promise<string> {
377
383
  return await this.getBinaryFromSdkRoot('apksigner.jar');
378
384
  }
379
385
 
@@ -382,12 +388,12 @@ export async function getApksignerForOs () {
382
388
  * META-INF folder recursively from the archive.
383
389
  * !!! The function overwrites the given apk after successful unsigning !!!
384
390
  *
385
- * @param {string} apkPath The path to the apk
386
- * @returns {Promise<boolean>} `true` if the apk has been successfully
391
+ * @param apkPath The path to the apk
392
+ * @returns `true` if the apk has been successfully
387
393
  * unsigned and overwritten
388
- * @throws {Error} if there was an error during the unsign operation
394
+ * @throws if there was an error during the unsign operation
389
395
  */
390
- export async function unsignApk (apkPath) {
396
+ export async function unsignApk (apkPath: string): Promise<boolean> {
391
397
  const tmpRoot = await tempDir.openDir();
392
398
  const metaInfFolderName = 'META-INF';
393
399
  try {
@@ -416,3 +422,4 @@ export async function unsignApk (apkPath) {
416
422
  }
417
423
 
418
424
  // #endregion
425
+