From 80363a9611b8d37477092a51ad6d506c0b86a4f7 Mon Sep 17 00:00:00 2001 From: Akos Birmacher <37296013+BirmacherAkos@users.noreply.github.com> Date: Fri, 9 Nov 2018 12:57:24 +0100 Subject: [PATCH] Fix Android App Bundle (.aab) signing. (#35) Fix the bug, which overrode the exported signed build artifact's ($BITRISE_SIGNED_APK_PATH) extension to `.apk` even if it was a `.abb`. --- bitrise.yml | 120 +++++++++++++++++++++------------------ keystore/keystore.go | 20 +++---- main.go | 132 ++++++++++++++++++++++--------------------- main_test.go | 10 ++-- step.yml | 15 +++-- 5 files changed, 155 insertions(+), 142 deletions(-) diff --git a/bitrise.yml b/bitrise.yml index e44786b..d0541a8 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1,4 +1,4 @@ -format_version: 5 +format_version: 6 default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git app: @@ -9,7 +9,6 @@ app: - SAMPLE_APP_REPOSITORY_URL: https://github.com/bitrise-samples/sample-apps-android-abi-split.git - BRANCH: master - - GRADLE_TASK: assembleRelease - GRADLEW_PATH: "./gradlew" # define these in your .bitrise.secrets.yml @@ -24,63 +23,32 @@ workflows: ci: before_run: - audit-this-step - - _go_tests - - create_test_apk steps: - - path::./: - is_skippable: true - title: Step Test - keystore pass == key pass - inputs: - - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_1_URL - - keystore_password: $BITRISEIO_ANDROID_KEYSTORE_PASSWORD_1 - - keystore_alias: $BITRISEIO_ANDROID_KEYSTORE_ALIAS_1 - - private_key_password: $BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD_1 - - path::./: - is_skippable: true - title: Step Test - keystore pass != key pass - inputs: - - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_2_URL - - keystore_password: $BITRISEIO_ANDROID_KEYSTORE_PASSWORD_2 - - keystore_alias: $BITRISEIO_ANDROID_KEYSTORE_ALIAS_2 - - private_key_password: $BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD_2 - - path::./: - is_skippable: true - title: Step Test - default alias - inputs: - - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_3_URL - - keystore_password: $BITRISEIO_ANDROID_KEYSTORE_PASSWORD_3 - - keystore_alias: $BITRISEIO_ANDROID_KEYSTORE_ALIAS_3 - - private_key_password: $BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD_3 - - path::./: - is_skippable: true - title: Step Test - android studio generated keystore (jks) - inputs: - - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_4_URL - - keystore_password: $BITRISEIO_ANDROID_KEYSTORE_PASSWORD_4 - - keystore_alias: $BITRISEIO_ANDROID_KEYSTORE_ALIAS_4 - - private_key_password: $BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD_4 + - go-list: + - golint: + - errcheck: + - go-test: + after_run: + - test_apk + - test_bundle - test: - before_run: - - audit-this-step - - _go_tests - - create_test_apk - steps: - - script: - inputs: - - content: |- - echo "BITRISE_APK_PATH: $BITRISE_APK_PATH" - - path::./: - title: Step Test - inputs: - - apk_path: $BITRISE_APK_PATH_LIST - - script: - inputs: - - content: |- - echo "BITRISE_SIGNED_APK_PATH: $BITRISE_SIGNED_APK_PATH" - echo "BITRISE_APK_PATH: $BITRISE_APK_PATH" + test_apk: + envs: + - GRADLE_TASK: assembleRelease + - APK_FILE_INCLUDE_FILTER: "*.apk" + after_run: + - create_build_artifact + - test + + test_bundle: + envs: + - GRADLE_TASK: bundleRelease + - APK_FILE_INCLUDE_FILTER: "*.aab" + after_run: + - create_build_artifact + - test - create_test_apk: + create_build_artifact: steps: - script: inputs: @@ -112,10 +80,50 @@ workflows: git remote add origin "${SAMPLE_APP_REPOSITORY_URL}" git fetch || exit 1 [[ -n "${COMMIT}" ]] && git checkout "${COMMIT}" || git checkout "${BRANCH}" + - install-missing-android-tools@2.3.3: + inputs: + - ndk_revision: '16' + run_if: ".IsCI" - gradle-runner: inputs: - gradle_task: "$GRADLE_TASK" - gradlew_path: "$GRADLEW_PATH" + - apk_file_include_filter: $APK_FILE_INCLUDE_FILTER + + test: + steps: + - path::./: + is_skippable: true + title: Step Test - keystore pass == key pass + inputs: + - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_1_URL + - keystore_password: $BITRISEIO_ANDROID_KEYSTORE_PASSWORD_1 + - keystore_alias: $BITRISEIO_ANDROID_KEYSTORE_ALIAS_1 + - private_key_password: $BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD_1 + - path::./: + is_skippable: true + title: Step Test - keystore pass != key pass + inputs: + - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_2_URL + - keystore_password: $BITRISEIO_ANDROID_KEYSTORE_PASSWORD_2 + - keystore_alias: $BITRISEIO_ANDROID_KEYSTORE_ALIAS_2 + - private_key_password: $BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD_2 + - path::./: + is_skippable: true + title: Step Test - default alias + inputs: + - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_3_URL + - keystore_password: $BITRISEIO_ANDROID_KEYSTORE_PASSWORD_3 + - keystore_alias: $BITRISEIO_ANDROID_KEYSTORE_ALIAS_3 + - private_key_password: $BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD_3 + - path::./: + is_skippable: true + title: Step Test - android studio generated keystore (jks) + inputs: + - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_4_URL + - keystore_password: $BITRISEIO_ANDROID_KEYSTORE_PASSWORD_4 + - keystore_alias: $BITRISEIO_ANDROID_KEYSTORE_ALIAS_4 + - private_key_password: $BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD_4 _go_tests: steps: diff --git a/keystore/keystore.go b/keystore/keystore.go index 3d50cb4..765bdef 100644 --- a/keystore/keystore.go +++ b/keystore/keystore.go @@ -109,7 +109,7 @@ func NewHelper(keystorePth, keystorePassword, alias string) (Helper, error) { }, nil } -func (helper Helper) createSignCmd(apkPth, destApkPth, privateKeyPassword string) ([]string, error) { +func (helper Helper) createSignCmd(buildArtifactPth, destBuildArtifactPth, privateKeyPassword string) ([]string, error) { split := strings.Split(helper.signatureAlgorithm, "with") if len(split) != 2 { return []string{}, fmt.Errorf("failed to parse signature algorithm: %s", helper.signatureAlgorithm) @@ -139,20 +139,20 @@ func (helper Helper) createSignCmd(apkPth, destApkPth, privateKeyPassword string cmdSlice = append(cmdSlice, "-keypass", privateKeyPassword) } - cmdSlice = append(cmdSlice, "-signedjar", destApkPth, apkPth, helper.alias) + cmdSlice = append(cmdSlice, "-signedjar", destBuildArtifactPth, buildArtifactPth, helper.alias) return cmdSlice, nil } -// SignAPK ... -func (helper Helper) SignAPK(apkPth, destApkPth, privateKeyPassword string) error { - if exist, err := pathutil.IsPathExists(apkPth); err != nil { +// SignBuildArtifact ... +func (helper Helper) SignBuildArtifact(buildArtifactPth, destBuildArtifactPth, privateKeyPassword string) error { + if exist, err := pathutil.IsPathExists(buildArtifactPth); err != nil { return err } else if !exist { - return fmt.Errorf("APK not exist at: %s", apkPth) + return fmt.Errorf("Build Artifact not exist at: %s", buildArtifactPth) } - cmdSlice, err := helper.createSignCmd(apkPth, destApkPth, privateKeyPassword) + cmdSlice, err := helper.createSignCmd(buildArtifactPth, destBuildArtifactPth, privateKeyPassword) if err != nil { return err } @@ -170,14 +170,14 @@ func (helper Helper) SignAPK(apkPth, destApkPth, privateKeyPassword string) erro return nil } -// VerifyAPK ... -func (helper Helper) VerifyAPK(apkPth string) error { +// VerifyBuildArtifact ... +func (helper Helper) VerifyBuildArtifact(buildArtifactPth string) error { cmdSlice := []string{ jarsigner, "-verify", "-verbose", "-certs", - apkPth, + buildArtifactPth, } prinatableCmd := command.PrintableCommandArgs(false, cmdSlice) diff --git a/main.go b/main.go index 52f1972..dd93715 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ var signingFileExts = []string{".mf", ".rsa", ".dsa", ".ec", ".sf"} // ConfigsModel ... type ConfigsModel struct { - ApkPath string + BuildArtifactPath string KeystoreURL string KeystorePassword string KeystoreAlias string @@ -40,7 +40,7 @@ type ConfigsModel struct { func createConfigsModelFromEnvs() ConfigsModel { return ConfigsModel{ - ApkPath: os.Getenv("apk_path"), + BuildArtifactPath: os.Getenv("apk_path"), KeystoreURL: os.Getenv("keystore_url"), KeystorePassword: os.Getenv("keystore_password"), KeystoreAlias: os.Getenv("keystore_alias"), @@ -52,7 +52,7 @@ func createConfigsModelFromEnvs() ConfigsModel { func (configs ConfigsModel) print() { fmt.Println() log.Infof("Configs:") - log.Printf(" - ApkPath: %s", configs.ApkPath) + log.Printf(" - ApkPath: %s", configs.BuildArtifactPath) log.Printf(" - KeystoreURL: %s", secureInput(configs.KeystoreURL)) log.Printf(" - KeystorePassword: %s", secureInput(configs.KeystorePassword)) log.Printf(" - KeystoreAlias: %s", configs.KeystoreAlias) @@ -63,16 +63,16 @@ func (configs ConfigsModel) print() { func (configs ConfigsModel) validate() error { // required - if configs.ApkPath == "" { + if configs.BuildArtifactPath == "" { return errors.New("no ApkPath parameter specified") } - apkPaths := strings.Split(configs.ApkPath, "|") - for _, apkPath := range apkPaths { - if exist, err := pathutil.IsPathExists(apkPath); err != nil { - return fmt.Errorf("failed to check if ApkPath exist at: %s, error: %s", apkPath, err) + buildArtifactPaths := strings.Split(configs.BuildArtifactPath, "|") + for _, buildArtifactPath := range buildArtifactPaths { + if exist, err := pathutil.IsPathExists(buildArtifactPath); err != nil { + return fmt.Errorf("failed to check if ApkPath exist at: %s, error: %s", buildArtifactPath, err) } else if !exist { - return fmt.Errorf("ApkPath not exist at: %s", apkPath) + return fmt.Errorf("ApkPath not exist at: %s", buildArtifactPath) } } @@ -177,7 +177,7 @@ func fileList(searchDir string) ([]string, error) { return fileList, nil } -func listFilesInAPK(aapt, pth string) ([]string, error) { +func listFilesInBuildArtifact(aapt, pth string) ([]string, error) { cmdSlice := []string{aapt, "list", pth} out, err := keystore.ExecuteForOutput(cmdSlice) if err != nil { @@ -210,7 +210,7 @@ func filterSigningFiles(fileList []string) []string { return signingFiles } -func removeFilesFromAPK(aapt, pth string, files []string) error { +func removeFilesFromBuildArtifact(aapt, pth string, files []string) error { cmdSlice := append([]string{aapt, "remove", pth}, files...) prinatableCmd := command.PrintableCommandArgs(false, cmdSlice) @@ -223,13 +223,13 @@ func removeFilesFromAPK(aapt, pth string, files []string) error { return err } -func isAPKSigned(aapt, pth string) (bool, error) { - filesInAPK, err := listFilesInAPK(aapt, pth) +func isBuildArtifactSigned(aapt, pth string) (bool, error) { + filesInBuildArtifact, err := listFilesInBuildArtifact(aapt, pth) if err != nil { return false, err } - metaFiles := filterMETAFiles(filesInAPK) + metaFiles := filterMETAFiles(filesInBuildArtifact) for _, metaFile := range metaFiles { ext := filepath.Ext(metaFile) @@ -240,24 +240,24 @@ func isAPKSigned(aapt, pth string) (bool, error) { return false, nil } -func unsignAPK(aapt, pth string) error { - filesInAPK, err := listFilesInAPK(aapt, pth) +func unsignBuildArtifact(aapt, pth string) error { + filesInBuildArtifact, err := listFilesInBuildArtifact(aapt, pth) if err != nil { return err } - metaFiles := filterMETAFiles(filesInAPK) + metaFiles := filterMETAFiles(filesInBuildArtifact) signingFiles := filterSigningFiles(metaFiles) if len(signingFiles) == 0 { - log.Printf("APK is not signed") + log.Printf("Build Artifact is not signed") return nil } - return removeFilesFromAPK(aapt, pth, signingFiles) + return removeFilesFromBuildArtifact(aapt, pth, signingFiles) } -func zipalignAPK(zipalign, pth, dstPth string) error { +func zipalignBuildArtifact(zipalign, pth, dstPth string) error { cmdSlice := []string{zipalign, "-f", "4", pth, dstPth} prinatableCmd := command.PrintableCommandArgs(false, cmdSlice) @@ -267,12 +267,12 @@ func zipalignAPK(zipalign, pth, dstPth string) error { return err } -func prettyAPKBasename(apkPth string) string { - apkBasenameWithExt := path.Base(apkPth) - apkExt := filepath.Ext(apkBasenameWithExt) - apkBasename := strings.TrimSuffix(apkBasenameWithExt, apkExt) - apkBasename = strings.TrimSuffix(apkBasename, "-unsigned") - return apkBasename +func prettyBuildArtifactBasename(buildArtifactPth string) string { + buildArtifactBasenameWithExt := path.Base(buildArtifactPth) + buildArtifactExt := filepath.Ext(buildArtifactBasenameWithExt) + buildArtifactBasename := strings.TrimSuffix(buildArtifactBasenameWithExt, buildArtifactExt) + buildArtifactBasename = strings.TrimSuffix(buildArtifactBasename, "-unsigned") + return buildArtifactBasename } func failf(format string, v ...interface{}) { @@ -291,7 +291,7 @@ func main() { } // Download keystore - tmpDir, err := pathutil.NormalizedOSTempDirPath("bitrise-sign-apk") + tmpDir, err := pathutil.NormalizedOSTempDirPath("bitrise-sign-build-artifact") if err != nil { failf("Failed to create tmp dir, error: %s", err) } @@ -341,75 +341,77 @@ func main() { log.Printf("zipalign: %s", zipalign) // --- - // Sign apks - apkPaths := strings.Split(configs.ApkPath, "|") - signedAPKPaths := make([]string, len(apkPaths)) + // Sign build artifacts + buildArtifactPaths := strings.Split(configs.BuildArtifactPath, "|") + signedBuildArtifactPaths := make([]string, len(buildArtifactPaths)) - log.Infof("signing %d apks", len(apkPaths)) + log.Infof("signing %d Build Artifacts", len(buildArtifactPaths)) fmt.Println() - for i, apkPath := range apkPaths { - log.Donef("%d/%d signing %s", i+1, len(apkPaths), apkPath) + for i, buildArtifactPath := range buildArtifactPaths { + artifactExt := path.Ext(buildArtifactPath) + log.Donef("%d/%d signing %s", i+1, len(buildArtifactPaths), buildArtifactPath) fmt.Println() - apkDir := path.Dir(apkPath) - apkBasename := prettyAPKBasename(apkPath) + buildArtifactDir := path.Dir(buildArtifactPath) + buildArtifactBasename := prettyBuildArtifactBasename(buildArtifactPath) - // unsign apk - unsignedAPKPth := filepath.Join(tmpDir, "unsigned.apk") - if err := command.CopyFile(apkPath, unsignedAPKPth); err != nil { - failf("Failed to copy apk, error: %s", err) + // unsign build artifact + unsignedBuildArtifactPth := filepath.Join(tmpDir, "unsigned"+artifactExt) + if err := command.CopyFile(buildArtifactPath, unsignedBuildArtifactPth); err != nil { + failf("Failed to copy build artifact, error: %s", err) } - isSigned, err := isAPKSigned(aapt, unsignedAPKPth) + isSigned, err := isBuildArtifactSigned(aapt, unsignedBuildArtifactPth) if err != nil { - failf("Failed to check if apk is signed, error: %s", err) + failf("Failed to check if build artifact is signed, error: %s", err) } if isSigned { - log.Printf("Signature file (DSA or RSA) found in META-INF, unsigning the apk...") - if err := unsignAPK(aapt, unsignedAPKPth); err != nil { - failf("Failed to unsign APK, error: %s", err) + log.Printf("Signature file (DSA or RSA) found in META-INF, unsigning the build artifact...") + if err := unsignBuildArtifact(aapt, unsignedBuildArtifactPth); err != nil { + failf("Failed to unsign Build Artifact, error: %s", err) } fmt.Println() } else { - log.Printf("No signature file (DSA or RSA) found in META-INF, skipping apk unsign...") + log.Printf("No signature file (DSA or RSA) found in META-INF, skipping build artifact unsign...") fmt.Println() } // --- - // sign apk - unalignedAPKPth := filepath.Join(tmpDir, "unaligned.apk") - log.Infof("Sign APK") - if err := keystore.SignAPK(unsignedAPKPth, unalignedAPKPth, configs.PrivateKeyPassword); err != nil { - failf("Failed to sign APK, error: %s", err) + // sign build artifact + unalignedBuildArtifactPth := filepath.Join(tmpDir, "unaligned"+artifactExt) + log.Infof("Sign Build Artifact") + if err := keystore.SignBuildArtifact(unsignedBuildArtifactPth, unalignedBuildArtifactPth, configs.PrivateKeyPassword); err != nil { + failf("Failed to sign Build Artifact, error: %s", err) } fmt.Println() - log.Infof("Verify APK") - if err := keystore.VerifyAPK(unalignedAPKPth); err != nil { - failf("Failed to verify APK, error: %s", err) + log.Infof("Verify Build Artifact") + if err := keystore.VerifyBuildArtifact(unalignedBuildArtifactPth); err != nil { + failf("Failed to verify Build Artifact, error: %s", err) } fmt.Println() - log.Infof("Zipalign APK") - signedAPKPaths[i] = filepath.Join(apkDir, apkBasename+"-bitrise-signed.apk") - if err := zipalignAPK(zipalign, unalignedAPKPth, signedAPKPaths[i]); err != nil { - failf("Failed to zipalign APK, error: %s", err) + log.Infof("Zipalign Build Artifact") + signedArtifactName := fmt.Sprintf("%s-bitrise-signed%s", buildArtifactBasename, artifactExt) + signedBuildArtifactPaths[i] = filepath.Join(buildArtifactDir, signedArtifactName) + if err := zipalignBuildArtifact(zipalign, unalignedBuildArtifactPth, signedBuildArtifactPaths[i]); err != nil { + failf("Failed to zipalign Build Artifact, error: %s", err) } fmt.Println() - // + // --- } - signedAPKPth := strings.Join(signedAPKPaths, "|") + signedBuildArtifactPth := strings.Join(signedBuildArtifactPaths, "|") - if err := tools.ExportEnvironmentWithEnvman("BITRISE_SIGNED_APK_PATH", signedAPKPth); err != nil { - log.Warnf("Failed to export APK, error: %s", err) + if err := tools.ExportEnvironmentWithEnvman("BITRISE_SIGNED_APK_PATH", signedBuildArtifactPth); err != nil { + log.Warnf("Failed to export Build Artifact, error: %s", err) } - log.Donef("The Signed APK path is now available in the Environment Variable: BITRISE_SIGNED_APK_PATH (value: %s)", signedAPKPth) + log.Donef("The Signed Build Artifact path is now available in the Environment Variable: BITRISE_SIGNED_APK_PATH (value: %s)", signedBuildArtifactPth) - if err := tools.ExportEnvironmentWithEnvman("BITRISE_APK_PATH", signedAPKPth); err != nil { - log.Warnf("Failed to export APK, error: %s", err) + if err := tools.ExportEnvironmentWithEnvman("BITRISE_APK_PATH", signedBuildArtifactPth); err != nil { + log.Warnf("Failed to export Build Artifact, error: %s", err) } - log.Donef("The Signed APK path is now available in the Environment Variable: BITRISE_APK_PATH (value: %s)", signedAPKPth) + log.Donef("The Signed Build Artifact path is now available in the Environment Variable: BITRISE_APK_PATH (value: %s)", signedBuildArtifactPth) } diff --git a/main_test.go b/main_test.go index 3f475ef..38d4729 100644 --- a/main_test.go +++ b/main_test.go @@ -6,11 +6,11 @@ import ( "github.com/stretchr/testify/require" ) -func TestPrettyAPKBasename(t *testing.T) { - require.Equal(t, "app", prettyAPKBasename("app-unsigned.apk")) - require.Equal(t, "app-signed", prettyAPKBasename("app-signed.apk")) - require.Equal(t, "app-debug", prettyAPKBasename("app-debug.apk")) - require.Equal(t, "app-release", prettyAPKBasename("app-release.apk")) +func TestPrettyBuildArtifactBasename(t *testing.T) { + require.Equal(t, "app", prettyBuildArtifactBasename("app-unsigned.apk")) + require.Equal(t, "app-signed", prettyBuildArtifactBasename("app-signed.apk")) + require.Equal(t, "app-debug", prettyBuildArtifactBasename("app-debug.apk")) + require.Equal(t, "app-release", prettyBuildArtifactBasename("app-release.apk")) } func TestFilterMETAFiles(t *testing.T) { diff --git a/step.yml b/step.yml index 2f777f3..2af7c15 100644 --- a/step.yml +++ b/step.yml @@ -32,17 +32,20 @@ toolkit: inputs: - apk_path: "$BITRISE_APK_PATH" opts: - title: "apk path" - summary: "" + title: "Build artifact path." + summary: "`Android App Bundle (.aab)` or `Android Aplication Package (.apk)`" description: |- - Path(s) to the APK file to sign. + Path(s) to the build artifact file to sign (`.aab` or `.apk`). - You can provide multiple APK file paths separated by `|` character. + You can provide multiple build artifact file paths separated by `|` character. Format examples: - `/path/to/my/app.apk` - `/path/to/my/app1.apk|/path/to/my/app2.apk|/path/to/my/app3.apk` + + - `/path/to/my/app.aab` + - `/path/to/my/app1.aab|/path/to/my/app2.apk|/path/to/my/app3.aab` is_required: true - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_URL opts: @@ -84,7 +87,7 @@ inputs: outputs: - BITRISE_SIGNED_APK_PATH: opts: - title: "Bitrise signed apk path" + title: "Bitrise signed build artifact path" - BITRISE_APK_PATH: opts: - title: "Bitrise signed apk path" + title: "Bitrise signed build artifact path"