Skip to content

Commit

Permalink
Prepare for release 1.6.1.
Browse files Browse the repository at this point in the history
  • Loading branch information
Iurii Makhno committed May 14, 2021
1 parent 950c8d6 commit cff808d
Show file tree
Hide file tree
Showing 33 changed files with 1,733 additions and 236 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ https://developer.android.com/studio/command-line/bundletool

## Releases

Latest release: [1.6.0](https://github.com/google/bundletool/releases)
Latest release: [1.6.1](https://github.com/google/bundletool/releases)
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ dependencies {
compile "com.android.tools.build:apkzlib:4.2.0-alpha13"
compile "com.android.tools.build:apksig:4.2.0-alpha13"
compile "com.android.tools.ddms:ddmlib:30.0.0-alpha10"
compile "com.android:zipflinger:4.2.0-alpha11"
compile "com.android:zipflinger:7.0.0-alpha14"

shadow "com.android.tools.build:aapt2-proto:4.1.0-alpha01-6193524"
shadow "com.google.auto.value:auto-value-annotations:1.6.2"
Expand All @@ -48,6 +48,7 @@ dependencies {
shadow "com.google.dagger:dagger:2.28.3"
annotationProcessor "com.google.dagger:dagger-compiler:2.28.3"
shadow "javax.inject:javax.inject:1"
shadow "org.bitbucket.b_c:jose4j:0.7.0"

compileWindows "com.android.tools.build:aapt2:4.1.0-alpha01-6193524:windows"
compileMacOs "com.android.tools.build:aapt2:4.1.0-alpha01-6193524:osx"
Expand Down Expand Up @@ -75,6 +76,7 @@ dependencies {
testCompile("org.smali:dexlib2:2.3.4") {
exclude group: "com.google.guava", module: "guava"
}
testCompile "org.bitbucket.b_c:jose4j:0.7.0"
}

def osName = System.getProperty("os.name").toLowerCase()
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
release_version = 1.6.0
release_version = 1.6.1
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.android.tools.build.bundletool.commands.AddTransparencyCommand;
import com.android.tools.build.bundletool.commands.BuildApksCommand;
import com.android.tools.build.bundletool.commands.BuildBundleCommand;
import com.android.tools.build.bundletool.commands.CheckTransparencyCommand;
import com.android.tools.build.bundletool.commands.CommandHelp;
import com.android.tools.build.bundletool.commands.DumpCommand;
import com.android.tools.build.bundletool.commands.ExtractApksCommand;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.android.tools.build.bundletool.commands;

import static org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA256;

import com.android.bundle.CodeTransparencyOuterClass.CodeTransparency;
import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription;
Expand All @@ -30,6 +31,7 @@
import com.android.tools.build.bundletool.model.utils.files.FilePreconditions;
import com.android.tools.build.bundletool.transparency.CodeTransparencyFactory;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import com.google.protobuf.InvalidProtocolBufferException;
Expand All @@ -38,16 +40,24 @@
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.Optional;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.keys.RsaKeyUtil;
import org.jose4j.lang.JoseException;
import org.jose4j.lang.UncheckedJoseException;

/** Command to add a Code Transparency File to Android App Bundle. */
@AutoValue
public abstract class AddTransparencyCommand {

public static final String COMMAND_NAME = "add-transparency";

static final int MIN_RSA_KEY_LENGTH = 3072;

private static final Flag<Path> BUNDLE_LOCATION_FLAG = Flag.path("bundle");

private static final Flag<Path> OUTPUT_FLAG = Flag.path("output");
Expand Down Expand Up @@ -117,13 +127,15 @@ public void execute() {
+ " one of the manifests.")
.build();
}
String jsonText =
toJsonText(CodeTransparencyFactory.createCodeTransparencyMetadata(inputBundle));
AppBundle.Builder bundleBuilder = inputBundle.toBuilder();
bundleBuilder.setBundleMetadata(
inputBundle.getBundleMetadata().toBuilder()
.addFile(
BundleMetadata.BUNDLETOOL_NAMESPACE,
BundleMetadata.TRANSPARENCY_FILE_NAME,
toJsonBytes(CodeTransparencyFactory.createCodeTransparencyMetadata(inputBundle)))
BundleMetadata.TRANSPARENCY_SIGNED_FILE_NAME,
toBytes(createJwsToken(jsonText)))
.build());
new AppBundleSerializer().writeToDisk(bundleBuilder.build(), getOutputPath());
} catch (ZipException e) {
Expand All @@ -133,6 +145,9 @@ public void execute() {
.build();
} catch (IOException e) {
throw new UncheckedIOException("An error occurred when processing the App Bundle.", e);
} catch (JoseException e) {
throw new UncheckedJoseException(
"An error occurred when signing the code transparency file.", e);
}
}

Expand Down Expand Up @@ -199,17 +214,40 @@ public static CommandHelp help() {
.build();
}

private static ByteSource toJsonBytes(CodeTransparency codeTransparency)
private String createJwsToken(String payload) throws JoseException {
JsonWebSignature jws = new JsonWebSignature();
jws.setAlgorithmHeaderValue(RSA_USING_SHA256);
jws.setCertificateChainHeaderValue(
getSignerConfig().getCertificates().toArray(new X509Certificate[0]));
jws.setPayload(payload);
jws.setKey(getSignerConfig().getPrivateKey());
return jws.getCompactSerialization();
}

private static String toJsonText(CodeTransparency codeTransparency)
throws InvalidProtocolBufferException {
return CharSource.wrap(JsonFormat.printer().print(codeTransparency))
.asByteSource(Charset.defaultCharset());
return JsonFormat.printer().print(codeTransparency);
}

private static ByteSource toBytes(String content) {
return CharSource.wrap(content).asByteSource(Charset.defaultCharset());
}

private void validateInputs() {
FilePreconditions.checkFileHasExtension("AAB file", getBundlePath(), ".aab");
FilePreconditions.checkFileExistsAndReadable(getBundlePath());
FilePreconditions.checkFileHasExtension("AAB file", getOutputPath(), ".aab");
FilePreconditions.checkFileDoesNotExist(getOutputPath());
Preconditions.checkArgument(
getSignerConfig().getPrivateKey().getAlgorithm().equals(RsaKeyUtil.RSA),
"Transparency signing key must be an RSA key, but %s key was provided.",
getSignerConfig().getPrivateKey().getAlgorithm());
int keyLength = ((RSAPrivateKey) getSignerConfig().getPrivateKey()).getModulus().bitLength();
Preconditions.checkArgument(
keyLength >= MIN_RSA_KEY_LENGTH,
"Minimum required key length is %s bits, but %s bit key was provided.",
MIN_RSA_KEY_LENGTH,
keyLength);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright (C) 2021 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
*/
package com.android.tools.build.bundletool.commands;

import static com.google.common.collect.ImmutableList.toImmutableList;

import com.android.tools.build.bundletool.io.TempDirectory;
import com.android.tools.build.bundletool.model.BundleMetadata;
import com.android.tools.build.bundletool.model.ZipPath;
import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException;
import com.android.tools.build.bundletool.model.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/** Checks code transparency for a given set of a device-specific APK files. */
final class ApkTransparencyChecker {

private static final String TRANSPARENCY_FILE_ZIP_ENTRY_NAME =
"META-INF/" + BundleMetadata.TRANSPARENCY_SIGNED_FILE_NAME;

static void checkTransparency(CheckTransparencyCommand command, PrintStream outputStream) {
try (TempDirectory tempDir = new TempDirectory("apk-transparency-checker")) {
ImmutableList<Path> allApkPaths =
extractAllApksFromZip(command.getApkZipPath().get(), tempDir);
Optional<Path> baseApkPath = getBaseApkPath(allApkPaths);
if (!baseApkPath.isPresent()) {
throw InvalidCommandException.builder()
.withInternalMessage(
"The provided .zip file must either contain a single APK, or, if multiple APK"
+ " files are present, a base APK.")
.build();
}

ZipFile baseApkFile = ZipUtils.openZipFile(baseApkPath.get());
Optional<ZipEntry> transparencyFileEntry =
Optional.ofNullable(baseApkFile.getEntry(TRANSPARENCY_FILE_ZIP_ENTRY_NAME));
if (!transparencyFileEntry.isPresent()) {
throw InvalidCommandException.builder()
.withInternalMessage(
"Could not verify code transparency because transparency file is not present in the"
+ " APK.")
.build();
}
} catch (IOException e) {
throw new UncheckedIOException("An error occurred when processing the file.", e);
}
}

/** Returns list of paths to all .apk files extracted from a .zip file. */
private static ImmutableList<Path> extractAllApksFromZip(
Path zipOfApksPath, TempDirectory tempDirectory) throws IOException {
ImmutableList.Builder<Path> allExtractedApkPaths = ImmutableList.builder();
Path zipExtractedSubDirectory = tempDirectory.getPath().resolve("extracted");
Files.createDirectory(zipExtractedSubDirectory);

ZipFile zipOfApks = ZipUtils.openZipFile(zipOfApksPath);
ImmutableList<ZipEntry> listOfApksToExtract =
zipOfApks.stream()
.filter(
zipEntry ->
!zipEntry.isDirectory()
&& zipEntry.getName().toLowerCase(Locale.ROOT).endsWith(".apk"))
.collect(toImmutableList());

for (ZipEntry apkToExtract : listOfApksToExtract) {
Path extractedApkPath =
zipExtractedSubDirectory.resolve(ZipPath.create(apkToExtract.getName()).toString());
Files.createDirectories(extractedApkPath.getParent());
try (InputStream inputStream = zipOfApks.getInputStream(apkToExtract);
OutputStream outputApk = Files.newOutputStream(extractedApkPath)) {
ByteStreams.copy(inputStream, outputApk);
allExtractedApkPaths.add(extractedApkPath);
}
}

return allExtractedApkPaths.build();
}

private static Optional<Path> getBaseApkPath(ImmutableList<Path> apkPaths) {
// If only 1 APK is present in the archive, it is assumed to be a universal or standalone APK.
if (apkPaths.size() == 1) {
return apkPaths.get(0).getFileName().toString().endsWith(".apk")
? Optional.of(apkPaths.get(0))
: Optional.empty();
}
return apkPaths.stream()
.filter(apkPath -> apkPath.getFileName().toString().equals("base.apk"))
.findAny();
}

private ApkTransparencyChecker() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ public void execute() {
AppBundle.buildFromModules(
modulesWithTargeting.build(), bundleConfig, getBundleMetadata());

Path outputDirectory = getOutputPath().getParent();
Path outputDirectory = getOutputPath().toAbsolutePath().getParent();
if (Files.notExists(outputDirectory)) {
logger.info("Output directory '" + outputDirectory + "' does not exist, creating it.");
FileUtils.createDirectories(outputDirectory);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2021 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
*/
package com.android.tools.build.bundletool.commands;

import com.android.tools.build.bundletool.model.AppBundle;
import com.android.tools.build.bundletool.model.BundleMetadata;
import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException;
import com.android.tools.build.bundletool.transparency.CodeTransparencyChecker;
import com.android.tools.build.bundletool.transparency.TransparencyCheckResult;
import com.google.common.io.ByteSource;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.util.Optional;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

/** Checks code transparency in a given bundle. */
final class BundleTransparencyChecker {

static void checkTransparency(CheckTransparencyCommand command, PrintStream outputStream) {
try (ZipFile bundleZip = new ZipFile(command.getBundlePath().get().toFile())) {
AppBundle inputBundle = AppBundle.buildFromZip(bundleZip);
Optional<ByteSource> signedTransparencyFile =
inputBundle
.getBundleMetadata()
.getFileAsByteSource(
BundleMetadata.BUNDLETOOL_NAMESPACE,
BundleMetadata.TRANSPARENCY_SIGNED_FILE_NAME);
if (!signedTransparencyFile.isPresent()) {
throw InvalidBundleException.builder()
.withUserMessage(
"Bundle does not include code transparency metadata. Run `add-transparency`"
+ " command to add code transparency metadata to the bundle.")
.build();
}
TransparencyCheckResult transparencyCheckResult =
CodeTransparencyChecker.checkTransparency(inputBundle, signedTransparencyFile.get());
if (!transparencyCheckResult.signatureVerified()) {
outputStream.print("Code transparency verification failed because signature is invalid.");
} else if (!transparencyCheckResult.fileContentsVerified()) {
outputStream.print(
"Code transparency verification failed because code was modified after transparency"
+ " metadata generation.\n"
+ transparencyCheckResult.getDiffAsString());
} else {
outputStream.print(
"Code transparency verified. Public key certificate fingerprint: "
+ transparencyCheckResult.certificateThumbprint().get());
}
} catch (ZipException e) {
throw InvalidBundleException.builder()
.withCause(e)
.withUserMessage("The App Bundle is not a valid zip file.")
.build();
} catch (IOException e) {
throw new UncheckedIOException("An error occurred when processing the App Bundle.", e);
}
}

private BundleTransparencyChecker() {}
}
Loading

0 comments on commit cff808d

Please sign in to comment.