DEV Community

Cover image for [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package com... do not match newer version; ignoring!]
Chan
Chan

Posted on

[INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package com... do not match newer version; ignoring!]

Development Build와 Production Build의 Certificate가 달라서 빌드에 실패하는 에러이다.

Steps To Reproduce

  1. usb로 local android device를 맥북에 연결하여 개발을 진행하고 있었다. expo run:android 명령을 실행할 때마다 usb로 연결된 기기에 앱이 설치되고 이를 실행해보면서 개발을 하였다.
  2. 최대한 closed testing에 참가하는 유저 수를 늘리기 위해 해당 device를 사용하여 closed testing을 진행해야하만 했다. 해당 기기에는 개발 빌드가 이미 설치되어 있었기에 중복해서 closed testing 앱을 설치할 수는 없었다. 따라서 개발 빌드를 uninstall하고 closed testing에 내놓은 앱의 버전을 플레이스토어에서 설치하였다.
  3. 그리고 다시 앱을 개발하기 위해 bun android 를 실행하였는데 다음과 같은 에러가 발생하였다.
   Error: adb: failed to install /Users/mac/study-up-frontend/android/app/build/outputs/apk/debug/app-debug.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package com.sleppynavigators.studyup signatures do not match newer version; ignoring!]
   Error: /Users/mac/Library/Android/sdk/platform-tools/adb -s R3CTC05E0JW install -r -d --user 0 /Users/mac/study-up-frontend/android/app/build/outputs/apk/debug/app-debug.apk exited with non-zero code: 1
   at ChildProcess.completionListener (/Users/mac/study-up-frontend/node_modules/@expo/spawn-async/src/spawnAsync.ts:67:13)
   at Object.onceWrapper (node:events:633:26)
   at ChildProcess.emit (node:events:518:28)
Enter fullscreen mode Exit fullscreen mode

Cause

Summary

  1. 네이티브 코드를 빌드하는 과정에서 private key를 통한 signing이 발생하고, public key를 통한 digital signature 확인이 이루어진다.
  2. 앱 설치/업데이트가 발생할 때 기존에 존재하는 public key를 포함한 certificate이 다운로드 받은 코드의 certicate과 다르다면, 개발자가 아닌 사람으로 부터 코드를 받았다고 간주하여 에러를 throw한다.
  3. 따라서 release build와 local build의 package attribute가 같은 상태에서는 keystore가 다르기에 문제가 발생한다.

expo run:android 의 동작 방식

Flowchart on expo run:android

💡 세번째 과정인 adb install 에서는 replace option(-r)을 사용하여 동일한 package attribute(application id)가 있는 apk를 uninstall 없이 자동으로 덮어쓴다.

Signing and Digital Signature Verification

Signing

💡 개발자는 앱의 소유주임을 증명할 수단으로 keystore를 가지고 있다. keystore는 private key와 public key가 들어있는 certificate을 가지고 있는 파일이다.

  1. 각각의 파일을 해시 함수를 사용하여 해시값으로 변환한다.
  2. 변환된 해시값을 key store의 private key를 이용하여 암호화한다. 암호화된 결과물을 digital signature라고 부른다.
  3. apk 완성한다. apk는 다음과 같이 구성된다.
    • .SF: 각각의 파일을 해시로 변환한 것
    • RSA: 디지털 서명
    • .X509: 공개키 인증서

Digital Signature Verification

앱 설치 및 업데이트 시, 패키지에 포함된 Digital Signature를 통해 이 APK가 이전 버전과 같은 키로 서명되었는지 검증한다.

Signing과 Digital Signature를 다이어그램으로 단계별로 나타내면 다음과 같다.

Flowchart on Signing Process

Flowchart on Verification Process

이 과정을 수행하는 목적은 다음과 같다.

  • 출처 검증: 새로 다운로드 받는 코드가 동일한 개발자(keystore 소유자)가 만든 것인지 확인
  • 무결정 검증: 앱 다운로드 또는 전송 중 코드가 변조 되는지 확인

💡 올바른 private key를 가지고 암호화를 해야, public key로 복호화를 했을 때 원래 가지고 있던 해쉬값을 얻어올 수 있다. 따라서 public key가 올바른 것이고, public key로 복호화를 했을 때 decripted_hash = computed_hash이면 해당 데이터가 본 주인으로부터 온 것임을 보일 수 있다.

에러 발생 지점

Compare hashes: production(release) build와 development build의 package attribute(applciation id)가 동일한데, 다른 private key를 사용하여 디지털 서명이 만들어졌기 때문에,

동일한 package attribute(application id)를 가진 application에 대하여 다른 public key로 복호화를 시도했기 때문에 decrypted hashes가 달라진다. 따라서 신용할 수 없는 주체에서 다운로드 받은 앱으로 간주하고 에러를 발생시킨다.

Package Attribute (Application Id)

안드로이드에서 package attribute는 app의 식별자 역할을 한다. 따라서 app.config.js 에서 development build, preview build, and production build의 package property value가 동일하다. 동일한 pakcage name(application id)를 가지고 있는데, 이미 설치된 production build와 development build의 signing key가 서로 달라서 앱을 덮어씌우지 못해 에러가 발생하게 된다.

Package Attribute(Newly Named Application Id)

The value of the package attribute in the APK's manifest file represents your app's universally unique application ID. It is formatted as a full Java-language-style package name for the Android app. The name can contain uppercase or lowercase letters, numbers, and underscores ('_'). However, individual package name parts can only start with letters.

Signing Key

apk(aab)에 디지털 서명을 하기 위해 사용하는 비대칭 키 쌍(private key, public key)이다.

apk vs. aab

aab는 구글 플레이에 출판되는 앱 형식이고, apk는 각 디바이스별로 최적화된 안드로이드 빌드 파일이다.

Solution

development, preview, production build의 이름을 각각 다르게 붙인다.

https://docs.expo.dev/build-reference/variants/, https://docs.expo.dev/tutorial/eas/multiple-app-variants/를 참고하면 된다.

const IS_DEV = process.env.APP_VARIANT === 'development';

export default {
  name: IS_DEV ? 'MyApp (Dev)' : 'MyApp',
  slug: 'my-app',
  ios: {
    bundleIdentifier: IS_DEV ? 'com.myapp.dev' : 'com.myapp',
  },
  android: {
    package: IS_DEV ? 'com.myapp.dev' : 'com.myapp',
  },
};
Enter fullscreen mode Exit fullscreen mode

development envrionment뿐만 아니라 여러개의 환경을 적용하고 싶다면, 다음과 같이 추상화할 수도 있다.

const IS_DEV = process.env.APP_VARIANT === 'development';
const IS_PREVIEW = process.env.APP_VARIANT === 'preview';

const getUniqueIdentifier = () => {
  if (IS_DEV) {
    return 'com.yourname.stickersmash.dev';
  }

  if (IS_PREVIEW) {
    return 'com.yourname.stickersmash.preview';
  }

  return 'com.yourname.stickersmash';
};

const getAppName = () => {
  if (IS_DEV) {
    return 'StickerSmash (Dev)';
  }

  if (IS_PREVIEW) {
    return 'StickerSmash (Preview)';
  }

  return 'StickerSmash: Emoji Stickers';
};

export default ({ config }) => ({
  ...config,
  name: getAppName(),
  ios: {
    ...config.ios,
    bundleIdentifier: getUniqueIdentifier(),
  },
  android: {
    ...config.android,
    package: getUniqueIdentifier(),
  },
});
Enter fullscreen mode Exit fullscreen mode

Limitation

local에서 만든 production build와 playstore에서 다운로드 받은 production build는 이름은 같은데 signature가 달라서 여전히 error를 throw한다. 이 경우에는 기존에 있던 build를 지우는 방식으로 해결할 수 밖에 없다.

Top comments (0)