The Social Project: Configurando a Assinatura do App no CI

Padrão

Se você ainda não conhece as motivações deste projeto, leia o primeiro artigo aqui.

Olá pessoal! No último artigo, configuramos o nosso build script para suportar o Kotlin. No post de hoje, vamos configurar a assinatura do app e levantar um ponto importante: como guardar seus segredos, principalmente quando o app é open source?

Com certeza, se você procurar um pouco, você encontrará diversos repositório com todo tipo de informação privada, desde chaves de APIs a ferramentas pagas. Todas, commitadas de forma acidental (ou mesmo proposital, mas de forma inadvertida) no repositório público do projeto.

No Android, um dos maiores “segredos” que mantemos é a chave de assinatura do app. Ela, juntamente com o Application ID, fazem um app se tornar único. Como o identificador é público, o vazamento de uma chave pode gerar um seríssimo risco de segurança, possibilitando que versões modificadas do seu app sejam passíveis de instalação sobre o app original, não só possibilitando comportamentos potencialmente perigosos para o usuário, como o acesso a informações internas e privadas que eventualmente o seu app tenha salvo localmente (como dados de autenticação na sua API, por exemplo).

A perda dessa chave também pode ser um problemão. Sem ela, é impossível enviar atualizações do seu app para o Google Play, por exemplo. Hoje felizmente já temos formas de minimizar esse risco, utilizando o Google Play App Signing (assunto que com certeza será abordado nessa série de posts).

Falando especificamente do nosso app, temos então um problema com relação a isso. Para a assinatura, precisamos de um arquivo (geralmente com a extensão .keystore ou .jks), duas senhas e um alias. O primeiro questionamento é sobre o arquivo em si. Colocaremos ele no repositório? Por mais que a chave necessite das senhas, ter o arquivo exposto envolve algum grau de risco.

Para solucionar esse problema temos basicamente duas abordagens: a primeira delas seria colocar o arquivo de assinatura em algum serviço externo, como Google Drive ou Dropbox, e baixá-lo no momento da build utilizando curl. Nesse cenário, teríamos uma chave de API exposta via variável de ambiente. Apesar de viável, esse cenário tem o problema de estarmos acoplando uma dependência externa ao simples processo de build do APK. Além de oscilações de rede que podem causar falha da build, também temos que manter essa request em dia, tendo que ajustar a chamada de download no caso de eventuais mudanças nas APIs desses serviços.

A segunda abordagem, e é a que vou utilizar aqui, será criptografar o arquivo e colocá-lo no repositório junto com o projeto. No CI, teremos exportado como variável de ambiente a chave que consegue descriptografar o arquivo, bem como as outras chaves necessárias para assinar o APK.

A maneira mais simples de gerarmos uma chave é através do próprio Android Studio.

Com a chave gerada, utilizaremos o openssl para criptografar o arquivo. Podemos fazer isso utilizando o comando:

openssl aes-256-cbc -e -in release.keystore -out release.keystore-cipher -md sha256 -k $CIPHER_DECRYPT_KEY

Perceba que no comando temos uma variável chamada CIPHER_DECRYPT_KEY, que é a chave que irá descriptografar o nosso arquivo lá no CI.

Com o arquivo criptografado gerado, vamos criar uma pasta no projeto para guardar as chaves. Além do arquivo criptografado release.keystore-cipher, também estou colocando o arquivo que assina as releases de debug. Isso é particularmente útil quando existe mais de uma pessoa trabalhando no projeto ou quando eventualmente há a necessidade de se trocar ou formatar a máquina, já que essa chave é gerada sempre que uma nova instalação da SDK do Android é realizada.

Com os arquivos no projeto, vamos primeiramente dizer ao CI para descriptografar o arquivo antes de executar a build. Perceba que adicionamos um passo chamado Decrypt release key ao nosso arquivo config.yml. Também modifiquei o passo de build para chamar a task assemble em vez da build (para validar a parte de assinatura de ambos os build variants):

version: 2
jobs:
  build:
    docker:
      - image: circleci/android:api-27-alpha

    working_directory: ~/social-app

    environment:
      JVM_OPTS: -Xmx3200m
      CIRCLE_JDK_VERSION: oraclejdk8

    steps:
      - checkout

      - restore_cache:
          key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}

      - run:
          name: Accept licenses
          command: yes | sdkmanager --licenses || true

      - run:
          name: Decrypt release key
          command: openssl aes-256-cbc -d -in distribution/release.keystore-cipher -out distribution/release.keystore -md sha256 -k $CIPHER_DECRYPT_KEY

      - run:
          name: Build
          command: ./gradlew clean check assemble assembleAndroidTest

      - save_cache:
          paths:
            - ~/.gradle
          key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}

Feito isso, vamos ao nosso build script configurar a assinatura e atribui-las para cada um dos build variants:

...

android {
  ...
  signingConfigs {
    debug {
      storeFile file("$rootDir/distribution/debug.keystore")
      storePassword 'android'
      keyAlias 'androiddebugkey'
      keyPassword 'android'
    }

    release {
      storeFile file("$rootDir/distribution/release.keystore")
      storePassword System.env.RELEASE_STORE_PASSWORD
      keyAlias System.env.RELEASE_KEY_ALIAS
      keyPassword System.env.RELEASE_KEY_PASSWORD
    }
  }

  buildTypes {
    debug {
      signingConfig signingConfigs.debug
    }

    release {
      signingConfig signingConfigs.release
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}

...

Perceba que a parte da assinatura de release vai pegar também os dados através de variáveis de ambiente, que serão adicionadas ao CircleCI:

E é isso! O Pull Request foi aberto e aprovado para este caso, tendo passado no CI e integrado na branch develop do repositório.

No próximo post vamos fazer a configuração do Firebase no projeto 🙂