Skip to content

Commit

Permalink
Adds bubblewrap fingerprint (#478)
Browse files Browse the repository at this point in the history
* Adds `bubblewrap fingerprint`

- Adds the fingerprint command that allows:
  - adding keys.
  - removing keys.
  - listing keys.
  - generating assetlinks.json.
- Removes assetlinks.json generation from `build`
  • Loading branch information
andreban authored Mar 4, 2021
1 parent 3fa21d6 commit 8ed223f
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 46 deletions.
3 changes: 3 additions & 0 deletions packages/cli/src/lib/Cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {BUBBLEWRAP_LOGO} from './constants';
import {updateConfig} from './cmds/updateConfig';
import {doctor} from './cmds/doctor';
import {merge} from './cmds/merge';
import {fingerprint} from './cmds/fingerprint';

export class Cli {
async run(args: string[]): Promise<boolean> {
Expand Down Expand Up @@ -79,6 +80,8 @@ export class Cli {
return await doctor();
case 'merge':
return await merge(parsedArgs);
case 'fingerprint':
return await fingerprint(parsedArgs);
default:
throw new Error(
`"${command}" is not a valid command! Use 'bubblewrap help' for a list of commands`);
Expand Down
39 changes: 2 additions & 37 deletions packages/cli/src/lib/cmds/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@
* limitations under the License.
*/

import {AndroidSdkTools, Config, DigitalAssetLinks, GradleWrapper, JdkHelper, KeyTool, Log,
import {AndroidSdkTools, Config, GradleWrapper, JdkHelper, KeyTool, Log,
ConsoleLog, TwaManifest, JarSigner, SigningKeyInfo, Result} from '@bubblewrap/core';
import * as path from 'path';
import * as fs from 'fs';
import {enUS as messages} from '../strings';
import {Prompt, InquirerPrompt} from '../Prompt';
import {PwaValidator, PwaValidationResult} from '@bubblewrap/validator';
import {printValidationResult} from '../pwaValidationHelper';
import {ParsedArgs} from 'minimist';
import {createValidateString} from '../inputHelpers';
import {TWA_MANIFEST_FILE_NAME} from '../constants';

// Path to the file generated when building an app bundle file using gradle.
const APP_BUNDLE_BUILD_OUTPUT_FILE_NAME = './app/build/outputs/bundle/release/app-release.aab';
Expand All @@ -38,9 +38,6 @@ const APK_SIGNED_FILE_NAME = './app-release-signed.apk';
// Output file for zipalign.
const APK_ALIGNED_FILE_NAME = './app-release-unsigned-aligned.apk';

const TWA_MANIFEST_FILE_NAME = './twa-manifest.json';
const ASSETLINKS_OUTPUT_FILE = './assetlinks.json';

interface SigningKeyPasswords {
keystorePassword: string;
keyPassword: string;
Expand Down Expand Up @@ -103,34 +100,6 @@ class Build {
}
}

async generateAssetLinks(
twaManifest: TwaManifest, passwords: SigningKeyPasswords): Promise<void> {
try {
const digitalAssetLinksFile = ASSETLINKS_OUTPUT_FILE;
const keyInfo = await this.keyTool.keyInfo({
path: twaManifest.signingKey.path,
alias: twaManifest.signingKey.alias,
keypassword: passwords.keyPassword,
password: passwords.keystorePassword,
});

const sha256Fingerprint = keyInfo.fingerprints.get('SHA256');
if (!sha256Fingerprint) {
this.prompt.printMessage(messages.messageSha256FingerprintNotFound);
return;
}

const digitalAssetLinks =
DigitalAssetLinks.generateAssetLinks(twaManifest.packageId, sha256Fingerprint);

await fs.promises.writeFile(digitalAssetLinksFile, digitalAssetLinks);

this.prompt.printMessage(messages.messageDigitalAssetLinksSuccess(digitalAssetLinksFile));
} catch (e) {
this.prompt.printMessage(messages.errorAssetLinksGeneration);
}
}

async buildApk(): Promise<void> {
await this.gradleWrapper.assembleRelease();
await this.androidSdkTools.zipalign(
Expand Down Expand Up @@ -199,10 +168,6 @@ class Build {
APP_BUNDLE_SIGNED_FILE_NAME;
this.prompt.printMessage(messages.messageAppBundleSuccess(appBundleFileName));

if (passwords) {
await this.generateAssetLinks(twaManifest, passwords);
}

if (validationPromise !== null) {
const result = await validationPromise;
if (result.isOk()) {
Expand Down
112 changes: 112 additions & 0 deletions packages/cli/src/lib/cmds/fingerprint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2021 Google Inc. All Rights Reserved.
*
* 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 {ParsedArgs} from 'minimist';
import {TwaManifest, DigitalAssetLinks, Fingerprint} from '@bubblewrap/core';
import {TWA_MANIFEST_FILE_NAME, ASSETLINKS_OUTPUT_FILE} from '../constants';
import {Prompt, InquirerPrompt} from '../Prompt';
import * as path from 'path';
import * as fs from 'fs';
import {enUS} from '../strings';
import {validateSha256Fingerprint} from '../inputHelpers';

async function loadManifest(args: ParsedArgs, prompt: Prompt): Promise<TwaManifest> {
const manifestFile = args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME);
prompt.printMessage(enUS.messageLoadingTwaManifestFrom(manifestFile));
if (!fs.existsSync(manifestFile)) {
throw new Error(enUS.errorCouldNotfindTwaManifest(manifestFile));
}
return await TwaManifest.fromFile(manifestFile);
}

async function saveManifest(
args: ParsedArgs, twaManifest: TwaManifest, prompt: Prompt): Promise<void> {
const manifestFile = args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME);
prompt.printMessage(enUS.messageSavingTwaManifestTo(manifestFile));
await twaManifest.saveToFile(manifestFile);
}

async function generateAssetLinks(
args: ParsedArgs, prompt: Prompt, twaManifest?: TwaManifest): Promise<boolean> {
twaManifest = twaManifest || await loadManifest(args, prompt);
const fingerprints = twaManifest.fingerprints.map((value) => value.value);
const digitalAssetLinks =
DigitalAssetLinks.generateAssetLinks(twaManifest.packageId, ...fingerprints);
const digitalAssetLinksFile = args.output || path.join(process.cwd(), ASSETLINKS_OUTPUT_FILE);
await fs.promises.writeFile(digitalAssetLinksFile, digitalAssetLinks);
prompt.printMessage(enUS.messageGeneratedAssetLinksFile(digitalAssetLinksFile));
return true;
}

async function addFingerprint(args: ParsedArgs, prompt: Prompt): Promise<boolean> {
if (args._.length < 3) {
throw new Error(enUS.errorMissingArgument(3, args._.length));
}
const fingerprintValue = (await validateSha256Fingerprint(args._[2])).unwrap();
const twaManifest = await loadManifest(args, prompt);
const fingerprint: Fingerprint = {name: args.name, value: fingerprintValue};
twaManifest.fingerprints.push(fingerprint);
prompt.printMessage(enUS.messageAddedFingerprint(fingerprint));
await saveManifest(args, twaManifest, prompt);
return await generateAssetLinks(args, prompt, twaManifest);
}

async function removeFingerprint(args: ParsedArgs, prompt: Prompt): Promise<boolean> {
if (args._.length < 3) {
throw new Error(enUS.errorMissingArgument(3, args._.length));
}
const fingerprint = args._[2];
const twaManifest = await loadManifest(args, prompt);
twaManifest.fingerprints =
twaManifest.fingerprints.filter((value) => {
if (value.value === fingerprint) {
prompt.printMessage(enUS.messageRemovedFingerprint(value));
return false;
}
return true;
});
await saveManifest(args, twaManifest, prompt);
return await generateAssetLinks(args, prompt, twaManifest);
}

async function listFingerprints(args: ParsedArgs, prompt: Prompt): Promise<boolean> {
const twaManifest = await loadManifest(args, prompt);
twaManifest.fingerprints.forEach((fingerprint) => {
console.log(`\t${fingerprint.name || '<unnamed>'}: ${fingerprint.value}`);
});
return true;
}

export async function fingerprint(
args: ParsedArgs,
prompt: Prompt = new InquirerPrompt()): Promise<boolean> {
if (args._.length < 2) {
throw new Error(enUS.errorMissingArgument(2, args._.length));
}
const subcommand = args._[1];
switch (subcommand) {
case 'add':
return await addFingerprint(args, prompt);
case 'remove':
return await removeFingerprint(args, prompt);
case 'list':
return await listFingerprints(args, prompt);
case 'generateAssetLinks':
return await generateAssetLinks(args, prompt);
default:
throw new Error(`Unknown subcommand: ${subcommand}`);
}
}
37 changes: 37 additions & 0 deletions packages/cli/src/lib/cmds/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const HELP_MESSAGES = new Map<string, string>(
'doctor .............. checks that the jdk and the androidSdk are in place and at the' +
' correct version',
'merge ............... merges your web manifest into twaManifest.json',
'fingerprint ......... generates the assetlinks.json file and manages keys',
].join('\n')],
['init', [
'Usage:',
Expand Down Expand Up @@ -121,6 +122,42 @@ const HELP_MESSAGES = new Map<string, string>(
'--ignore [fields-list]................. the fields which you would like to keep the same.',
'You can enter each key from your Web Manifest.',
].join('\n')],
['fingerprint', [
'Usage:',
'',
'',
'bubblewrap fingerprint [subcommand]',
'',
' Global fingerprint flags: ',
' --manifest=<manifest> ........ Path to the Trusted Web Activity configuration.',
'',
'',
' - add: adds a fingerprint to the project configuration.',
' Usage:',
' bubblewrap fingerprint add [SHA-256 fingerprint] <flags>',
'',
' Flags:',
' --name=<name> ...... a name for the fingerprint',
'',
'',
' - remove: removes a fingerprint from the project configuration.',
' Usage:',
' bubblewrap fingerprint remove [SHA-256 fingerprint] <flags>',
'',
'',
' - list: lists the fingerprints in the project configuration',
' Usage:',
' bubblewrap fingerprint list <flags>',
'',
'',
' - generateAssetLinks: Generates an AssetLinks file from the project configuration.',
' Usage:',
' bubblewrap generateAssetLinks <flags>',

This comment has been minimized.

Copy link
@elgreg

elgreg Mar 17, 2021

Contributor

This missing the command fingerprint. Should it be bubblewrap fingerprint generateAssetLinks <flags> instead?

'',
' Flags:',
' --output=<name> .... path from where to load the project configuration.',
'',
].join('\n')],
],
);

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import {magenta} from 'colors';

export const APP_NAME = 'bubblewrap-cli';
export const ASSETLINKS_OUTPUT_FILE = './assetlinks.json';
export const BUBBLEWRAP_LOGO = magenta(
/* eslint-disable indent */
`,-----. ,--. ,--. ,--.
Expand All @@ -26,3 +27,4 @@ export const BUBBLEWRAP_LOGO = magenta(
\`------' \`----' \`---' \`---'\`--'\`----'--' '--\`--' \`--\`--| |-'
\`--\' `);
/* eslint-enable indent */
export const TWA_MANIFEST_FILE_NAME = './twa-manifest.json';
15 changes: 15 additions & 0 deletions packages/cli/src/lib/inputHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,18 @@ export async function validatePackageId(input: string): Promise<Result<string, E

return Result.ok(input);
}

/**
* A {@link ValidateFunction} that receives a {@link string} as input and resolves to a
* {@link string} when successful. Verifies if the input is a valid SHA-256 fingerprint.
* @param {string} input a string representing a SHA-256 fingerprint.
* @returns {Result<string, Error>} a result that resolves to a {@link string} on
* success or {@link Error} on failure.
*/
export async function validateSha256Fingerprint(input: string): Promise<Result<string, Error>> {
input = input.toUpperCase();
if (input.match(/^([0-9A-F]{2}:){31}[0-9A-F]{2}$/)) {
return Result.ok(input);
}
return Result.error(new Error(messages.errorInvalidSha256Fingerprint(input)));
}
Loading

0 comments on commit 8ed223f

Please sign in to comment.