diff --git a/.buildkite/commands/diff-merged-manifest.sh b/.buildkite/commands/diff-merged-manifest.sh index 43d5582aebdf..d7bda1234d67 100755 --- a/.buildkite/commands/diff-merged-manifest.sh +++ b/.buildkite/commands/diff-merged-manifest.sh @@ -14,9 +14,6 @@ BUILD_VARIANT=$1 echo "--- :rubygems: Setting up Gems" install_gems -echo "--- :closed_lock_with_key: Installing Secrets" -bundle exec fastlane run configure_apply - echo "--- 💾 Diff Merged Manifest (Module: WooCommerce, Build Variant: ${BUILD_VARIANT})" comment_with_manifest_diff "WooCommerce" ${BUILD_VARIANT} diff --git a/.buildkite/commands/git-crypt-unlock.sh b/.buildkite/commands/git-crypt-unlock.sh new file mode 100755 index 000000000000..2e9d27616706 --- /dev/null +++ b/.buildkite/commands/git-crypt-unlock.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -euo pipefail + +echo "$GIT_CRYPT_ENCRYPTION_KEY" | base64 -d | git-crypt unlock - diff --git a/.buildkite/commands/gradle-cache-build.sh b/.buildkite/commands/gradle-cache-build.sh index 322de2857604..3a1582393ac3 100755 --- a/.buildkite/commands/gradle-cache-build.sh +++ b/.buildkite/commands/gradle-cache-build.sh @@ -13,8 +13,8 @@ fi echo "--- :rubygems: Setting up Gems" install_gems -echo "--- :closed_lock_with_key: Installing Secrets" -bundle exec fastlane run configure_apply +echo "--- :closed_lock_with_key: Decrypting Secrets" +.buildkite/commands/git-crypt-unlock.sh echo "--- :hammer_and_wrench: Building" ./gradlew assembleWasabiDebug diff --git a/.buildkite/commands/prototype-build.sh b/.buildkite/commands/prototype-build.sh index c4054f8ba101..a6afd0990853 100755 --- a/.buildkite/commands/prototype-build.sh +++ b/.buildkite/commands/prototype-build.sh @@ -12,8 +12,8 @@ APP_TO_BUILD="${1?You need to specify the app to build, WooCommerce or WooCommer echo "--- :rubygems: Setting up Gems" install_gems -echo "--- :closed_lock_with_key: Installing Secrets" -bundle exec fastlane run configure_apply +echo "--- :closed_lock_with_key: Decrypting Secrets" +.buildkite/commands/git-crypt-unlock.sh echo "--- :hammer_and_wrench: Building ${APP_TO_BUILD}" bundle exec fastlane build_and_upload_prototype_build app:"${APP_TO_BUILD}" diff --git a/.buildkite/commands/release-build.sh b/.buildkite/commands/release-build.sh index dd79a8182573..70f5f7e37d42 100755 --- a/.buildkite/commands/release-build.sh +++ b/.buildkite/commands/release-build.sh @@ -5,8 +5,8 @@ APP_TO_BUILD="${1?You need to specify the app to build, WooCommerce or WooCommer echo "--- :rubygems: Setting up Gems" install_gems -echo "--- :closed_lock_with_key: Installing Secrets" -bundle exec fastlane run configure_apply +echo "--- :closed_lock_with_key: Decrypting Secrets" +.buildkite/commands/git-crypt-unlock.sh echo "--- :hammer_and_wrench: Building ${APP_TO_BUILD}" bundle exec fastlane build_and_upload_google_play app:"${APP_TO_BUILD}" diff --git a/.buildkite/commands/run-instrumented-tests.sh b/.buildkite/commands/run-instrumented-tests.sh index a4dbd8f51738..359f68f6f120 100755 --- a/.buildkite/commands/run-instrumented-tests.sh +++ b/.buildkite/commands/run-instrumented-tests.sh @@ -11,8 +11,8 @@ fi echo "--- :rubygems: Setting up Gems" install_gems -echo "--- :closed_lock_with_key: Installing Secrets" -bundle exec fastlane run configure_apply +echo "--- :closed_lock_with_key: Decrypting Secrets" +.buildkite/commands/git-crypt-unlock.sh echo "--- 🧪 Testing" set +e diff --git a/.buildkite/commands/run-unit-tests.sh b/.buildkite/commands/run-unit-tests.sh index e2e3fce5016a..4aac5f1504df 100755 --- a/.buildkite/commands/run-unit-tests.sh +++ b/.buildkite/commands/run-unit-tests.sh @@ -11,8 +11,8 @@ fi echo "--- :rubygems: Setting up Gems" install_gems -echo "--- :closed_lock_with_key: Installing Secrets" -bundle exec fastlane run configure_apply +echo "--- :closed_lock_with_key: Decrypting Secrets" +.buildkite/commands/git-crypt-unlock.sh echo "+++ 🧪 Testing" set +e diff --git a/.buildkite/release-pipelines/download-release-translations.yml b/.buildkite/release-pipelines/download-release-translations.yml index ad66488110b3..35871f14e2a6 100644 --- a/.buildkite/release-pipelines/download-release-translations.yml +++ b/.buildkite/release-pipelines/download-release-translations.yml @@ -13,6 +13,9 @@ steps: echo '--- :ruby: Setup Ruby Tools' install_gems + echo '--- :closed_lock_with_key: Decrypting Secrets' + .buildkite/commands/git-crypt-unlock.sh + echo '--- :globe_with_meridians: Download Release Translations' bundle exec fastlane download_release_translations skip_confirm:true include_wear_app:"${INCLUDE_WEAR_APP:-false}" agents: diff --git a/.buildkite/release-pipelines/finalize-release.yml b/.buildkite/release-pipelines/finalize-release.yml index 65a9a4975cab..ec1f180f4097 100644 --- a/.buildkite/release-pipelines/finalize-release.yml +++ b/.buildkite/release-pipelines/finalize-release.yml @@ -13,6 +13,9 @@ steps: echo '--- :ruby: Setup Ruby Tools' install_gems + echo '--- :closed_lock_with_key: Decrypting Secrets' + .buildkite/commands/git-crypt-unlock.sh + echo '--- :shipit: Finalize Release' bundle exec fastlane finalize_release skip_confirm:true include_wear_app:"${INCLUDE_WEAR_APP:-false}" agents: diff --git a/.buildkite/shared-pipeline-vars b/.buildkite/shared-pipeline-vars index 08d9e22a5a23..cf41135f8150 100644 --- a/.buildkite/shared-pipeline-vars +++ b/.buildkite/shared-pipeline-vars @@ -3,6 +3,7 @@ # This file is `source`'d before calling `buildkite-agent pipeline upload`, and can be used # to set up some variables that will be interpolated in the `.yml` pipeline before uploading it. -export CI_TOOLKIT="automattic/a8c-ci-toolkit#5.4.0" +# "git-crypt-unlock" branch / https://github.com/Automattic/a8c-ci-toolkit-buildkite-plugin/pull/195 +export CI_TOOLKIT="automattic/a8c-ci-toolkit#0a3f10921096cee57c18ac5667fc64c1aaad4a7d" export TEST_COLLECTOR="test-collector#v1.10.1" export CLAUDE_PLUGIN="claude-summarize#v1.1.0" diff --git a/.configure b/.configure deleted file mode 100644 index bf5458276191..000000000000 --- a/.configure +++ /dev/null @@ -1,43 +0,0 @@ -{ - "project_name": "woocommerce-android", - "branch": "trunk", - "pinned_hash": "811d08531b187cfd2400e99d27214bba8906425d", - "files_to_copy": [ - { - "file": "android/WCAndroid/secrets.properties", - "destination": "~/.configure/woocommerce-android/secrets/secrets.properties", - "encrypt": true - }, - { - "file": "android/WCAndroid/google-services.json", - "destination": "WooCommerce/google-services.json", - "encrypt": true - }, - { - "file": "android/WCAndroid/sentry.properties", - "destination": "sentry.properties", - "encrypt": true - }, - { - "file": "android/debug.keystore", - "destination": "~/.configure/woocommerce-android/secrets/debug_a8c.keystore", - "encrypt": true - }, - { - "file": "android/automattic_upload.jks", - "destination": "WooCommerce/upload.jks", - "encrypt": true - }, - { - "file": "android/WCAndroid/google-upload-credentials.json", - "destination": "~/.configure/woocommerce-android/secrets/google-upload-credentials.json", - "encrypt": true - }, - { - "file": "android/WCAndroid/firebase.secrets.json", - "destination": ".configure-files/firebase.secrets.json", - "encrypt": true - } - ], - "file_dependencies": [] -} \ No newline at end of file diff --git a/.configure-files/automattic_upload.jks.enc b/.configure-files/automattic_upload.jks.enc deleted file mode 100644 index e04c021b8b5f..000000000000 Binary files a/.configure-files/automattic_upload.jks.enc and /dev/null differ diff --git a/.configure-files/debug.keystore.enc b/.configure-files/debug.keystore.enc deleted file mode 100644 index 55ef352b6f37..000000000000 Binary files a/.configure-files/debug.keystore.enc and /dev/null differ diff --git a/.configure-files/firebase.secrets.json.enc b/.configure-files/firebase.secrets.json.enc deleted file mode 100644 index 9a004dc5f58c..000000000000 Binary files a/.configure-files/firebase.secrets.json.enc and /dev/null differ diff --git a/.configure-files/google-services.json.enc b/.configure-files/google-services.json.enc deleted file mode 100644 index a8d92f5f1eca..000000000000 Binary files a/.configure-files/google-services.json.enc and /dev/null differ diff --git a/.configure-files/google-upload-credentials.json.enc b/.configure-files/google-upload-credentials.json.enc deleted file mode 100644 index de5014cad268..000000000000 Binary files a/.configure-files/google-upload-credentials.json.enc and /dev/null differ diff --git a/.configure-files/gradle.properties.enc b/.configure-files/gradle.properties.enc deleted file mode 100644 index 7f4065aa9a91..000000000000 Binary files a/.configure-files/gradle.properties.enc and /dev/null differ diff --git a/.configure-files/secrets.properties.enc b/.configure-files/secrets.properties.enc deleted file mode 100644 index d6de02973489..000000000000 Binary files a/.configure-files/secrets.properties.enc and /dev/null differ diff --git a/.configure-files/sentry.properties.enc b/.configure-files/sentry.properties.enc deleted file mode 100644 index 526952438d12..000000000000 Binary files a/.configure-files/sentry.properties.enc and /dev/null differ diff --git a/.gitattributes b/.gitattributes index 34a0b3db47d5..315b670c08c7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,13 @@ RELEASE-NOTES.txt merge=union -.configure-files/*.enc binary +######################################### +# Secrets files encrypted with git-crypt +######################################### + +secrets.properties filter=git-crypt diff=git-crypt +sentry.properties filter=git-crypt diff=git-crypt +google-services.json filter=git-crypt diff=git-crypt +firebase.secrets.json filter=git-crypt diff=git-crypt +google-upload-credentials.json filter=git-crypt diff=git-crypt +*.keystore filter=git-crypt diff=git-crypt +*.jks filter=git-crypt diff=git-crypt diff --git a/.gitignore b/.gitignore index 71ab405bd2bd..5b500d6f8360 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,6 @@ developer.properties # Crash Logging Configuration fabric.properties -# Sentry -sentry.properties # Local configuration file (sdk path, etc) local.properties @@ -66,15 +64,9 @@ captures/ # Android Studio backup files projectFilesBackup/ -# Keystore files -*.jks - # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild -# Google Services (e.g. APIs or Firebase) -google-services.json - # Silver Searcher ignore file .agignore @@ -88,7 +80,6 @@ google-services.json fastlane/README.md fastlane/report.xml fastlane/.env -google-upload-credentials.json fastlane/screenshots fastlane/promo_sceenshots # This is a byproduct of the screenshots composition process @@ -102,15 +93,6 @@ default.profraw local-builds.gradle -# All secrets should be stored under .configure-files -# Everything without a .enc extension is ignored -.configure-files/* -!.configure-files/*.enc -# This secret is not part of the repository anymore, but we keep it in the -# gitignore for retrocompatibility, so that it won't appear as a new file and -# be accidentally checked in the repository. -google-upload-credentials.json - # Kotlin .kotlin/ diff --git a/README.md b/README.md index b7522e63bcfb..c041bef8b7bc 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,14 @@ $ cd woocommerce-android ``` -1. Copy `defaults.properties` to the secrets directory: `cp defaults.properties ~/.configure/woocommerce-android/secrets/secrets.properties`. See the [Configuration Files](docs/project-overview.md#configuration-files) section for a breakdown of the properties. -1. Generate the developer oauth2 tokens. These values get copied into the `~/.configure/woocommerce-android/secrets.properties` file in the next step. See the [OAuth2 Authentication](docs/project-overview.md#oauth2-authentication) section for details. +1. If you are a developer at Automattic: + 1. Make sure you have `git-crypt` installed (`brew install git-crypt`) + 1. Search for "WooCommerce Android git-crypt encryption key" in our Secret Store, and copy the Base64 value in your clipboard + 1. Run `pbpaste | base64 -d | git-crypt unlock -` to decrypt the encrypted files (including `secrets.properties` and `WooCommerce/google-services.json`) +1. If you are an external contributor: + 1. Generate developer OAuth2 tokens. See the [OAuth2 Authentication](docs/project-overview.md#oauth2-authentication) section for details. + 1. Edit `defaults.properties` and adjust the values as needed—especiallyincluding `wp.oauth.*` ones. See the [Configuration Files](docs/project-overview.md#configuration-files) section for a breakdown of the properties. + 1. `cp WooCommerce/google-services.json-example WooCommerce/google-services.json` (to replace that encrypted file with placeholder content) 1. In Android Studio, open the project from the local repository. This will auto-generate `local.properties` with the SDK location. 1. Optional: Go to Tools → Device Manager and create an emulated device. 1. Run. (Creates a default virtual device if you skipped the previous step) diff --git a/WooCommerce/build.gradle b/WooCommerce/build.gradle index 2c5c6b168ecc..3f0b915a5446 100644 --- a/WooCommerce/build.gradle +++ b/WooCommerce/build.gradle @@ -17,7 +17,7 @@ plugins { fladle { variant = "vanillaDebug" - serviceAccountCredentials = rootProject.file(".configure-files/firebase.secrets.json") + serviceAccountCredentials = rootProject.file("firebase.secrets.json") testTargets = [ "notPackage com.woocommerce.android.e2e.tests.screenshot", "notClass com.woocommerce.android.e2e.tests.ui.OrdersRealAPI", @@ -496,12 +496,13 @@ android.buildTypes.all { buildType -> } // If Google services file doesn't exist, copy example file. - if (!file("google-services.json").exists()) { + def googleServicesFile = file("google-services.json") + if (!googleServicesFile.exists() || isFileEncrypted(googleServicesFile)) { tasks.copyGoogleServicesExampleFile.copy() } // Print warning message if example Google services file is used. - if ((file("google-services.json").text) == (file("google-services.json-example").text)) { + if ((googleServicesFile.text) == (file("google-services.json-example").text)) { println("WARNING: You're using the example google-services.json file. Google login will fail.") } } @@ -514,6 +515,13 @@ static def loadPropertiesFromFile(inputFile) { return properties } +static def isFileEncrypted(File file) { + def gitcryptHeader = [0x00, 0x47, 0x49, 0x54, 0x43, 0x52, 0x59, 0x50, 0x54] as byte[] // GITCRYPT header + def header = new byte[gitcryptHeader.length] + file.withInputStream { stream -> stream.read(header) } + return Arrays.equals(header, gitcryptHeader) +} + def isLeakCanaryEnabled() { return developerProperties.get("enable_leak_canary") ?: true } diff --git a/WooCommerce/google-services.json b/WooCommerce/google-services.json new file mode 100644 index 000000000000..76af8b26d3d3 Binary files /dev/null and b/WooCommerce/google-services.json differ diff --git a/WooCommerce/upload.jks b/WooCommerce/upload.jks new file mode 100644 index 000000000000..b922c9efb45c Binary files /dev/null and b/WooCommerce/upload.jks differ diff --git a/debug.keystore b/debug.keystore new file mode 100644 index 000000000000..9b1d0ad05296 Binary files /dev/null and b/debug.keystore differ diff --git a/docs/project-overview.md b/docs/project-overview.md index dcdb5c572ca5..050d26125ed4 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -13,11 +13,8 @@ When creating your application, you should select "**Native client**" for the ap The "**Website URL**", "**Redirect URLs**", and "**Javascript Origins**" fields are required but not used for the mobile apps. Just use "**[https://localhost](https://localhost)**". -Once you've created your application in the [applications manager][wp-com-apps], you'll -need to update the `wc.oauth.app_id` and `wc.oauth.app_secret` fields in `secrets.properties`. -See [setup instructions][setup] for more details about secrets file. Then you can compile and run the app on a device or an emulator and -try to login with a WordPress.com account. Note that authenticating to WordPress.com via Google is -not supported in development builds of the app, only in the official release. +Once you've created your application in the [applications manager][wp-com-apps], you'll need to update the `wc.oauth.app_id` and `wc.oauth.app_secret` fields in `defaults.properties` (copied from `defaults-example.properties`). See [setup instructions][setup] for more details. +Then you can compile and run the app on a device or an emulator and try to login with a WordPress.com account. Note that authenticating to WordPress.com via Google is not supported in development builds of the app, only in the official release. Note that credentials created with our [WordPress.com applications manager][wp-com-apps] allow login only and not signup. New accounts must be created using the [official app][wp-app] @@ -35,7 +32,15 @@ Read more about [OAuth2][oauth] and the [WordPress.com REST endpoint][wp-api]. #### `secrets.properties` -The `secrets.properties` file is used to store sensitive information that should not be checked into version control. This file is located at `~/.configure/woocommerce-android/secrets/secrets.properties`. +The `secrets.properties` file is used to store sensitive information that should not be checked into version control in clear text. +This file is encrypted (using `git-crypt`), and only developers working at Automattic have the decryption key. + +If you are a developer working at Automattic, ensure you followed those instructions once after cloning the repo: + 1. Make sure you have `git-crypt` installed (`brew install git-crypt`) + 1. Search for "WooCommerce Android git-crypt encryption key" in our Secret Store, and copy the Base64 value in your clipboard + 1. Run `pbpaste | base64 -d | git-crypt unlock -` to decrypt the encrypted files (including `secrets.properties`) + +If you are an external contributor, provide those variables in your `defaults.properties` instead: | Property | Description | |:---------------------------|:------------| diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 81f82e050c06..d1547003968d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -62,7 +62,7 @@ GLOTPRESS_APP_STRINGS_PROJECT_URL = 'https://translate.wordpress.com/projects/wo GLOTPRESS_PLAYSTORE_METADATA_PROJECT_URL = "#{GLOTPRESS_APP_STRINGS_PROJECT_URL}/release-notes/".freeze APP_PACKAGE_NAME = 'com.woocommerce.android' -GOOGLE_FIREBASE_SECRETS_PATH = File.join(PROJECT_ROOT_FOLDER, '.configure-files', 'firebase.secrets.json') +GOOGLE_FIREBASE_SECRETS_PATH = File.join(PROJECT_ROOT_FOLDER, 'firebase.secrets.json') # Instantiate versioning classes VERSION_CALCULATOR = Fastlane::Wpmreleasetoolkit::Versioning::MarketingVersionCalculator.new @@ -80,7 +80,7 @@ DEFAULT_BRANCH = 'trunk' REPOSITORY_NAME = 'woocommerce-android' GH_ORG_NAME = 'woocommerce' -UPLOAD_TO_PLAY_STORE_JSON_KEY = File.join(Dir.home, '.configure', 'woocommerce-android', 'secrets', 'google-upload-credentials.json') +UPLOAD_TO_PLAY_STORE_JSON_KEY = File.join(PROJECT_ROOT_FOLDER, 'google-upload-credentials.json') SUPPORTED_LOCALES = [ { glotpress: 'ar', android: 'ar', google_play: 'ar', promo_config: {} }, @@ -458,8 +458,6 @@ platform :android do UI.important("Downloading latest translations for release: #{release_version_current}") UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") unless skip_confirm || UI.confirm('Do you want to continue?') - configure_apply(force: is_ci) - # Don't check translation coverage in CI check_translation_progress_all unless is_ci download_translations @@ -493,8 +491,6 @@ platform :android do UI.important("Finalizing release: #{release_version_current}") UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") unless skip_confirm || UI.confirm('Do you want to continue?') - configure_apply(force: is_ci) - # Bump the release version and build code UI.message 'Bumping final release version and build code...' VERSION_FILE.write_version( @@ -1250,8 +1246,11 @@ platform :android do end def firebase_secret(name:) - UI.user_error!('Unable to locale Firebase Secrets File – did you run `configure apply`?') unless File.file? GOOGLE_FIREBASE_SECRETS_PATH - key_file_secrets = JSON.parse(File.read(GOOGLE_FIREBASE_SECRETS_PATH)) + begin + key_file_secrets = JSON.parse(File.read(GOOGLE_FIREBASE_SECRETS_PATH)) + rescue StandardError + UI.user_error!('Unable to read Firebase Secrets File – did you run `echo "…encryption-key…" | base64 -d | git-crypt unlock -` on the repo?') + end UI.user_error!("Unable to find key `#{name}` in #{GOOGLE_FIREBASE_SECRETS_PATH}") if key_file_secrets[name].nil? key_file_secrets[name] end diff --git a/firebase.secrets.json b/firebase.secrets.json new file mode 100644 index 000000000000..bcf2a8820f5b Binary files /dev/null and b/firebase.secrets.json differ diff --git a/google-upload-credentials.json b/google-upload-credentials.json new file mode 100644 index 000000000000..2e4689258fa5 Binary files /dev/null and b/google-upload-credentials.json differ diff --git a/secrets.properties b/secrets.properties new file mode 100644 index 000000000000..fbff9a9afd0c Binary files /dev/null and b/secrets.properties differ diff --git a/sentry.properties b/sentry.properties new file mode 100644 index 000000000000..921722cb7bd3 Binary files /dev/null and b/sentry.properties differ diff --git a/settings.gradle b/settings.gradle index f532378ae9c5..2a5e5d0de62b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,7 +23,7 @@ ext { gradle.ext { isCi = System.getenv('CI')?.toBoolean() ?: false - secretsPath = "${System.getProperty("user.home")}/.configure/woocommerce-android/secrets/secrets.properties" + secretsPath = "secrets.properties" secretProperties = loadPropertiesWithFallback( logger, file("${rootDir}/defaults.properties"), @@ -40,7 +40,12 @@ static def loadPropertiesWithFallback(Logger logger, File fallbackFile, File pri def defaultProperties = readPropertiesFromFile(fallbackFile) def primaryProperties if (primaryFile.exists()) { - primaryProperties = readPropertiesFromFile(primaryFile) + try { + primaryProperties = readPropertiesFromFile(primaryFile) + } catch (Exception e) { + logger.warn("Failed to parse primary properties file: ${primaryFile}. Using fallback: ${fallbackFile}. Error: ${e}") + primaryProperties = new Properties() + } } else { logger.warn("Primary properties file not found: ${primaryFile}. Using fallback: ${fallbackFile}.") primaryProperties = new Properties() @@ -93,8 +98,14 @@ def checkForRemoteBuildCacheOptimizedExperience() { } def assertSecretsApplied() { - if (!file(gradle.ext.secretsPath).exists()) { - throw new GradleException("The build requested remote build cache, but secrets file is not found. Please run `bundle exec fastlane run configure_apply` to apply secrets.") + def secretsFile = file(gradle.ext.secretsPath) + if (!secretsFile.exists()) { + throw new GradleException("The build requested remote build cache, but the `secrets.properties` file is not found. See README.md for instructions on how to set those up.") + } + try { + readPropertiesFromFile(secretsFile) + } catch (Exception ex) { + throw new GradleException("The `secrets.properties` file exists but could not be read as a valid `.properties` file. This likely means it is still encrypted (git-crypt locked). Please decrypt it before building. See README.md for instructions.", ex) } }