Last Updated on 3월 25th, 2021, By
 In AppSealing News, 앱실링 블로그

2013년 페이스북 해커톤에서 아이디어가 나왔고, 2015년에 대중을 위한 오픈 소스 플랫폼으로 출시된 것이 이제는 앱 개발에 있어 가장 널리 사용되는 프레임워크 중 하나가 되었습니다. React Native는 자바스크립트를 기반으로하며 개발자가 iOS와 Android 운영 체제 모두에 대해 기본적으로 렌더링된 모바일 앱을 빌드할 수 있도록 합니다. 앱과 웹 사이트용 UI 인터페이스와 컴포넌트를 빌드하기 위한 오픈 소스 프론트엔드 자바스크립트 라이브러리인 React JavaScript(ReactJS)를 사용합니다. 본 글에서는 오픈 소스 모바일 애플리케이션 프레임워크로 구축된 앱에 대한 React Native 보안에 대해서 설명합니다. 

React Native 보안

모든 자바스크립트 기반 프레임워크와 마찬가지로 React Native는 보안 위협에 취약합니다. React Native 보호 관점에서의 분석은 프레임워크의 다른 부분과 이들 간의 연결을 고려해야 합니다. 애플리케이션의 소스코드는 클라이언트단에서 소스 코드를 얼마든지 볼 수 있기 때문에, 본질적으로 프런트엔드 JS 애플리케이션은 수정되거나 민감한 데이터가 쉽게 유출될 수 있습니다. JS 애플리케이션의 가장 일반적인 보안 문제는 다음과 같습니다.

크로스사이트 스크립팅(XSS; Cross-site Scripting)

XSS 공격이라고도 하며 공격자가 웹 사이트를 속여 사용자의 브라우저에서 임의의 JS 코드를 실행하도록 할 때 발생합니다. XSS 공격에는 두 가지 종류가 있습니다. Reflected XSS는 일부 텍스트 정보가 포함된 링크가 브라우저 상에서 코드로 처리될 때 발생합니다. Stored XSS는 공격자가 서버 액세스 권한을 얻고 클라이언트의 웹 페이지 정보를 생성할 수 있는 코드가 서버에서 실행될 때 발생합니다. 

불안전한 무작위성과 링크

이것은 링크가 클라이언트가 입력한 데이터를 기반으로 하고, 공격자가 원래 JS 코드에 악성 코드를 추가할 때 발생합니다. 링크를 클릭하면 브라우저에서 공격자의 스크립트가 실행됩니다.

서버측 렌더링 공격자에 의해서 제어되는 ​​초기 상태

이것은 앱이 서버측에서 렌더링되는 경우에 발생합니다. 페이지의 기본 버전을 생성하면 JSON 문자열에서 문서 변수를 생성할 수 있습니다. JSON.stringify () 함수에 제공된 모든 데이터가 페이지에서 볼 수 있는 문자열로 변환되기 때문에 위험할 수 있습니다.

임의 코드 실행(ACE)

이것은 공격자가 임의 코드 실행 익스플로잇이라는 프로그램을 사용하여 대상 프로세스에 임의의 명령을 실행할 수 있을 때 발생합니다. 이는 제품의 모든 사용자가 악성코드에 노출되므로 매우 해로울 수 있습니다.

Zip 슬립

이 위협은 코드 라이브러리의 보안이 손상되고, 공격자가 대상 디렉토리 외부에서 악성 코드나 파일을 압축 해제할 때 발생합니다. 이로 인해 공격자는 중요한 시스템 또는 구성 파일을 덮어쓸 수도 있습니다.

따라서 앱을 빌드할 때 악의적인 제3자로부터 React Native 애플리케이션을 보호하는 것이 매우 중요합니다. React Native에는 민감한 정보의 안전한 저장을 지원하는 내장된 방법을 제공하고 있지 않지만, Android나 iOS 플랫폼 모두에서 앱 보안을 강화하는데 사용할 수 있는 몇 가지 솔루션이 있습니다. 아래에서 이들 중 일부에 대해 자세히 설명합니다.

앱과 서버간 연결 보안

React Native에서 클라이언트와 서버간의 통신은 오픈 플랫폼이 가진 보안 취약점 때문에 반드시 보안이 필요합니다. 클라이언트와 서버간의 통신에 가장 일반적으로 사용되는 웹 서비스는 HTTP 기반 REST 입니다. HTTPS 연결에서 가장 중요한 두 가지는 핸드 셰이킹 중에 서버에서 제공하는 유효한 인증서와 전송 중에 데이터 암호화에 사용되는 암호입니다. 인증서는 서버의 ID를 증명하는 역할을 합니다. 클라이언트에 저장된 신뢰할 수 있는 인증 기관(CA)에서 서명한 유효한 인증서를 서버가 제공할 수 없다면 연결이 중단됩니다. 이 메커니즘은 공격자가 사용자 디바이스에 악의적인 루트 CA 인증서를 설치하여 클라이언트가 공격자가 서명한 CA 인증서를 신뢰하도록 속이거나 디바이스에 저장된 CA를 완전히 손상시키는 방법으로 악용될 수 있습니다. 사용자 자격 증명의 인증 불일치를 방지하기 위해서, 별도의 코드 변수를 가진 다른 사용자에게 영역 속성을 할당해야 한다는 점을 유의해야 합니다. 서버 응답 메커니즘과 영역 속성 사이의 작은 불일치 조차도 앱의 보안을 손상시키고 권한이 없는 사용자에게 액세스를 허용할 수 있습니다. 

React Native에서 SSL 피닝(Pinning)

SSL(Secure Sockets Layer)은 네트워킹 컴퓨터간에 인증 및 암호화된 링크를 설정하는 프로토콜입니다. TLS(Transport Layer Security) 프로토콜은 1999년 SSL에 대한 업데이트였지만 종종 “SSL” 또는 “SSL/TLS”로 통칭됩니다. SSL 피닝은 SSL 핸드 셰이킹 후에도 클라이언트 측에서 서버 인증서의 유효성을 검사하는데 사용되는 기술을 의미합니다. 신뢰할 수 있는 인증서 목록은 런타임 동안 서버 인증서와 비교할 수 있도록 클라이언트 앱에 내장됩니다. 서버 인증서가 로컬 인증서 목록과 일치하지 않으면 연결이 즉시 중단되고 데이터가 서버로 전송되지 않습니다. 이렇게 하면 클라이언트가 신뢰할 수 있는 서버에만 연결됩니다. 그러나 고정된 인증서가 만료되면 이후 인증서는 릴리스 전에 클라이언트 애플리케이션에 고정시켜야 합니다. 만약 업데이트된 인증서가 고정되지 않으면, 해당 인증서는 클라이언트에서 인식되지 않으며 추가 통신이 종료됩니다. 이를 “앱 브릭킹(App Bricking)”이라고 합니다. SSL 피닝은 일반적으로 전체 인증서를 고정하거나 해시된 키만 고정하는 두 가지 방법으로 수행할 수 있습니다. 후자는 업데이트된 인증서에 서명하고 브릭킹 인스턴스를 방지하는데 동일한 키를 사용할 수 있으므로 더 바람직한 옵션입니다.

인증서 피닝은 Android에서 세 가지 방법으로 구현할 수 있습니다.

1. 네트워크 보안 구성

이것은 Android에서 인증서를 피닝하는 기본 방법입니다. 이것은 간단한 방법이며 코딩이 필요하지 않습니다. 여기서 유일한 걸림돌은 Android N 이상에서만 지원된다는 것입니다. 이를 통해 개발자는 앱 코드를 수정하지 않고도 도메인 및 앱에 특정한 보안 구성 파일의 보안 설정을 커스터마이징할 수 있습니다. 구성은 res\xml 디렉토리에 생성된 XML 파일을 사용합니다. XML 파일은 다음과 같이 선언될 수 있습니다.

<?xml version=”1.0″ encoding=”utf-8″?>
<manifest … >
    <application
        android:networkSecurityConfig=”@xml/network_security_config”
         … >
       …
   </application>
</manifest>

이제  <base-config> <domain-config>태그를 사용하여 네트워크 보안 파일을 구성할 수 있습니다.

  • <base-config>는 연결을 유지하는 도메인에 관계없이 전체 앱에 적용해야 하는 구성을 선언합니다.
  • <domain-config>는 개발자가 선택한 도메인에 특정한 규칙을 구성합니다.

2. TrustManager

TrustManager는 피어가 제공한 자격 증명을 앱에서 허용할 지 여부를 결정합니다. javax.net.ssl의 이 기술은 다음 단계를 통해 인증서 피닝에 사용할 수 있습니다.

  • 가급적이면 주석 줄이 없는 PEM 또는 DER 형식의 인증서 파일을 디렉터리에 추가합니다.
  • 인증서로 KeyStore 초기화합니다.

val resource_stream = resources.openRawResource(R.raw.cert)
val key_store_type = KeyStore.getDefaultType()
val key_store = KeyStore.getInstance(key_store_type)
key_store.load(resource_stream, null)

  • TrustManager 인스턴스를 생성합니다.

val trust_manager_algorithm = TrustManagerFactory.getDefaultAlgorithm()
val trust_manager_factory = TrustManagerFactory.getInstance(trust_manager_algorithm)
trust_manager_factory.init(keyStore)

  • 마지막 단계는 TLS 프로토콜을 사용하여 SSL 컨텍스트를 만드는 것입니다. 그런 다음 TrustManager를 사용하여 보안 SSL 연결을 생성합니다. 

val ssl_context = SSLContext.getInstance(“TLS”)
ssl_context.init(null, trust_manager_factory.trustManagers, null)
val url = URL(“http://www.secureexample.com/“)
val url_connection = url.openConnection() as HttpsURLConnection
url_connection.sslSocketFactory = ssl_context.socketFactory

3. OkHttp 및 인증서 피닝

이것은 Square의 유명한 라이브러리로 사용이 쉬우며, 네트워킹을 위해 Retrofit에서 사용합니다. 시작하려면 OktHtp CertificatePinner 빌더에서 인증서 피닝 인스턴스를 생성하고, 해당 도메인과 지문을 여기에 추가합니다. 마지막 단계는 빌더를 OkHttp 클라이언트에 추가하는 것입니다.

val certificatePinner = CertificatePinner.Builder()
       .add(
               “www.secureexample.com”,
               “sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=”
       ).build()
val okHttpClient = OkHttpClient.Builder()
       .certificatePinner(certificatePinner)
       .build()

현재 지문이 만료되는 경우 여러 개의 지문을 빌더에 추가할 수도 있습니다. TrustManager 경우 처럼 인증서 파일을 리소스 폴더로 가져올 수도 있습니다. 다음 단계는 파일에서 지문을 추출할 클래스를 작성하는 것입니다. 지문은 gradle 파일에서 build-config 필드로 언급되어야 합니다.

react-native-pinch 또는 react-native-ssl-pinning과 같은 React Native 라이브러리를 사용하거나 네이티브 메소드를 사용하여 React Native 애플리케이션에서 SSL 피닝을 구현하는 두 가지 방법이 있습니다. 네이티브 플랫폼에서의 구현은 Android와 iOS에서 다릅니다. 첫 번째 단계는 openssl s_client -showcerts -servername <domain(i.e. google.com)> -connect <domain(i.e. google.com)>:443 명령을 사용하여 도메인에 대한 인증서를 생성하는 것입니다.

출력은 체인의 전체 인증서(상단의 리프 인증서와 하단의 루트 인증서)입니다.

iOS 플랫폼의 경우 내장 스크립트(get_pin_from_certificate.py)가 포함된 TrustKit이 일반적으로 SSL 피닝에 사용됩니다. 파일을 다운로드하고 $python get_pin_from_certificate.py <your .pem file> 명령을 사용하여 공개키 해시를 검색하십시오.

SSL 피닝을 위해 자체 서명된 리프, 중개 또는 루트 인증서를 추가할 수 있습니다. 리프 인증서는 인증서가 클라이언트에 속해있고 연결이 안전하다는 것을 완전히 보장합니다. 인증서의 만료 시간이 매우 짧기 때문에 서버와의 연결이 끊어지지 않도록 앱을 업데이트 해야 합니다. 리프 인증서의 경우 백업 키를 사용하는 것이 좋습니다. 중개 인증서는 중개 인증 기관에 따라 다릅니다. 이 방법을 사용하는 장점은 리프 인증서에 대한 모든 변경 사항이 앱에서 자동으로 업데이트된다는 것입니다. 그러나 공급자를 신뢰할 수 있는 경우에만 중개 인증서를 사용해야 합니다. 루트 인증서는 루트 인증 기관에서 승인한 모든 중개 인증서에 종속됩니다. 이는 중개 인증서의 보안 위반으로 인해 앱이 해커의 공격에 취약해질 수 있으므로 상대적으로 안전하지 않은 방법이라고 할 수 있습니다. 

React Native 저장소 보안

개발자는 종종 애플리케이션 내부에 영구 데이터를 저장합니다. React Native는 Async-storage, sqlite, pouchdb 및 realm과 같은 여러 가지 방법을 제공하고 있습니다. 

다음은 애플리케이션에 보안 계층을 추가 할 수 있는 몇 가지 플러그인입니다.

  • SQLite : 데이터를 저장하는 가장 일반적인 방법 중 하나입니다. SQLCipher는 SQLite 암호화를 위해 널리 사용되는 오픈 소스 확장 컴포넌트입니다. 암호는 키 없이는 액세스 할 수 없는 256비트 AES로 암호화됩니다. React Native에는 SQLCipher를 제공하는 react-native-sqlcipher-2 (ORM 공급자로 pouchdb 와 함께 사용 가능)와 react-native-sqlcipher-storage (Cordova 구현 기반) 라이브러리가 있습니다.
  • Realm : SQLite에 비해 더 빠른 데이터베이스이며 기본적으로 암호화를 지원합니다. 또한 AES256 알고리즘을 사용하며 암호화는 SHA-2 HMAC 해시를 사용하여 검증합니다.

이 외에도 React Native는 iOS 및 Android를 위해서 민감한 데이터를 저장하는 몇 가지 기본 방법을 제공합니다. 아래에서 안전한 저장소를 제공하는 React Native 라이브러리에 대해서 설명합니다.

iOS : KeyChain 서비스

프로그래머가 정보를 안전하게 저장하는데 도움이 되도록 Apple은 KeyChain 서비스라는 보안 프레임워크를 제공합니다. 이러한 서비스를 사용하여 앱은 KeyChain이라는 암호화된 데이터베이스에 작은 사용자 데이터 청크를 저장할 수 있습니다. KeyChain은 주로 암호화된 데이터베이스(SecKeychain 클래스)와 데이터베이스에 삽입된 항목 (SecKeychainItem 클래스)의 두 부분으로 구성됩니다. 비밀번호, 인증서, 신용 카드 정보, 토큰 등과 같은 민감한 정보를 여기에 저장할 수 있습니다. react-native-keychain 라이브러리는 React Native 애플리케이션에 대한 KeyChain/KeyStore 액세스를 제공합니다.

키 체인은 다음 명령을 실행하여 React-Native-Keychain-Library를 통해서 쉽게 설치할 수 있습니다.
yarn add react-native-keychain

그런 다음, 다음 명령을 사용하여 코드 라이브러리를 KeyChain에 연결할 수 있습니다.
react-native link react-native-keychain

MainApplication.java에 라이브러리가 추가되었는지 확인하여 성공적인 연결을 확인할 수 있습니다.
그런 다음, 다음을 사용하여 애플리케이션을 다시 빌드할 수 있습니다.
react-native run-ios
react-native run-android

위의 단계를 완료하면 react-native-keychain-library를 앱 내에서 사용할 수 있습니다.  setGenericPassword(username, password, [{ accessControl, accessible, accessGroup, service }]) 는 사용자 이름/암호 조합을 보안 저장소에 저장하고 항목이 있는 경우  true 를 반환합니다. 또한 이 함수는 문자열 값만 저장할 수 있습니다. 객체 값이 있는 자격 증명은 JSON.stringify를 사용하여 저장해야 합니다.

getGenericPassword([{ authenticationPrompt, service }]) 는 보안 저장소에서 사용자 이름/암호 조합을 검색합니다. 이 함수는 항목이 있으면 { username, password }를 반환하고 그렇지 않으면 false 를 반환합니다. 권한 관련 오류가 있는 경우 기능이 거부됩니다. setGenericPassword와 마찬가지로 이 함수도 문자열만 검색할 수 있으며, 객체 형식의 자격 증명은 JSON.parse를 사용하여 액세스 할 수 있습니다.

사용자 이름/암호 조합은 resetGenericPassword함수를 사용하여 보안 저장소에서 제거하거나 재설정 할 수 있습니다.

requestSharedWebCredentials() 함수는 iOS 사용자가 사용할 수 있으며 공유 웹 자격 증명을 선택할 수 있습니다. 앱과 서버에 추가 설정이 필요합니다. 이 함수는 승인된 경우 { server, username, password }를 반환하고 거부된 경우 false 를 반환합니다. 플랫폼이 기능을 지원하지 않거나 공유 자격 증명이 없는 경우 오류 메시지가 표시됩니다.

setSharedWebCredentials(server, username, password) 는 공유 웹 자격 증명을 설정하는데 사용되며 함수가 승인되면 true를 반환합니다.

setInternetCredentials(server, username, password, [{ accessControl, accessible, accessGroup }]) 함수는 사용자 이름 및 비밀번호와 함께 서버 이름을 보안 저장소에 저장하는데 사용됩니다.

getInternetCredentials(server, [{ authenticationPrompt }])는 보안 저장소에서 서버/사용자 이름/암호를 검색합니다.

resetInternetCredentials(server) 함수는 보안 저장소에서 서버/사용자 이름/비밀번호 조합을 완전히 제거하고 재설정합니다.

canImplyAuthentication([{ authenticationType }]) 함수는 사용자가 선택한 설정으로 디바이스에서 인증 정책이 지원되는지 확인하는데 사용됩니다. setter 함수에서 사용할 수 있는 accessControl함수와 함께 사용해야 합니다. 이 함수는 승인되면 true를 반환합니다. 

getSupportedBiometryType() 은 디바이스에서 사용할 수 있는 하드웨어 생체 인식 지원에 대해 알려줍니다. 지원되는 경우 Keychain.BIOMETRY_TYPE 열거형으로 확인되거나 null이 표시됩니다. Android 및 iOS 기기 모두에서 작동합니다. 반환되는 생체 인식 유형은 다음과 같습니다.

TOUCH_ID (iOS only)
FACE_ID (iOS only)
FINGERPRINT (Android only)

API는 또한 KeyChain에 액세스 할 수 있는 시점을 결정하는 Keychain.ACCESSIBLE 열거형을 제공합니다. 사용자는 애플리케이션 유형 및 정보의 민감도에 따라 옵션을 선택할 수 있습니다. 일반적으로 사용되는 몇 가지 옵션은 다음과 같습니다.

WHEN_UNLOCKED: 디바이스가 잠금 해제된 경우에만 KeyChain의 데이터를 액세스 할 수 있습니다. 

ALWAYS: KeyChain을 항상 액세스 할 수 있습니다. 하지만 안전하지 유의해야 합니다. 

WHEN_UNLOCKED_THIS_DEVICE_ONLY: KeyChain은 특정 디바이스에서 잠금이 해제된 경우에만 액세스 할 수 있습니다. 이 열거형에 속한 항목은 다른 디바이스로 마이그레이션 할 수 없습니다.

ALWAYS_THIS_DEVICE_ONLY: KeyChain은 디바이스의 잠금 해제 여부에 관계없이 특정 디바이스에 있는 경우에만 액세스 할 수 있습니다.

AFTER_FIRST_UNLOCK: 디바이스를 다시 시작한 후 잠금을 해제하지 않으면 KeyChain의 데이터에 액세스 할 수 없습니다.

AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: 특정 디바이스에서 처음 잠금을 해제한 후에 만 ​​KeyChain에 액세스 할 수 있습니다. 디바이스가 잠금 해제되지 않으면 다시 시작한 후에도 액세스 할 수 없습니다. 이것이 아마도 가장 안전한 옵션 일 것입니다. 이 속성이 있는 항목도 다른 디바이스로 마이그레이션되지 않습니다.

WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeyChain의 데이터는 디바이스가 잠금 해제된 경우에만 액세스 할 수 있습니다. 이 옵션은 디바이스에 암호가 설정된 경우에만 사용할 수 있습니다.

Keychain.ACCESS_CONTROL enum열거형은 KeyChain의 항목에 액세스 할 수 있는 사람을 결정하는 옵션을 제공합니다. 다음 옵션을 사용하여 KeyChain 항목에 대한 접근을 제어 할 수 있습니다.

USER_PRESENCE Touch ID 또는 비밀번호로 항목에 액세스 할 수 있습니다.
BIOMETRY_ANY 오직 등록된 모든 손가락의 Touch ID 만으로 항목에 액세스 할 수 있습니다.
BIOMETRY_CURRENT_SET 현재 등록된 손가락에 대해서만 Touch ID로 항목에 액세스 할 수 있습니다.
DEVICE_PASSCODE 비밀번호로 항목에 액세스 할 수 있습니다.
APPLICATION_PASSWORD 데이터 암호화 키 생성을 위해 애플리케이션 제공 비밀번호를 사용하는 제약이 있습니다.
BIOMETRY_ANY_OR_DEVICE_PASSCODE 등록된 모든 손가락의 Touch ID 또는 비밀번호를 사용하여 항목에 액세스 할 수 있습니다.
BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE 현재 등록된 손가락의 Touch ID 또는 비밀번호에 대해서만 항목에 액세스 할 수 있는 제약이 있습니다.

 

Android: 안전한 SharedPreferences

Android에서는 키-값 데이터 저장소에 해당하는 것을 SharedPreferences라고 명명합니다. 영구적인  키-값 기본 데이터 유형을 저장하고 검색할 수 있는 프레임워크를 제공합니다. 그러나 SharedPreferences 는 데이터를 일반 텍스트로 저장하므로 잠재적인 위협으로부터 데이터를 보호하려면 중요한 데이터를 암호화해야 합니다. Android Keystore를 사용하여 SharedPreferences 에 대한 자체 암호화 래퍼를 작성할 수 있지만, 더 쉽고 안전한 옵션은 보다 간단하고 사용하기 쉬운 UI를 제공하는 AndroidX 보안 라이브러리에 암호화된 공유 환경 설정을 저장하는 것입니다. AndroidX 보안 라이브러리를 시작하기 위한 첫 번째 단계는 모듈 수준 build.gradle 파일에 종속성 implementation”androidx.security:security-crypto:1.0.0-alpha03″ 을 추가하는 것입니다. 라이브러리가 현재 알파 단계에 있으므로 API의 일부가 향후 버전에서 변경되거나 제거될 수 있음을 염두에 두어야 합니다. 종속성을 추가한 후 암호화 마스터 키를 생성하여 Android Keystore에 저장해야 합니다.
EncryptedSharedPreferences 인스턴스를 만들 위치에 다음 코드를 배치하면 쉽게 수행할 수 있습니다.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec).

기본 사양인 AES256_GCM_SPEC이 마스터 키를 생성하기 위해 제공됩니다. 이 사양을 사용하는 것이 좋지만 키 생성에 대한 추가 제어가 필요한 경우 KeyGenParameterSpec을 제공하는 옵션도 있습니다. 마지막으로 SharedPreferences 를 감싸고 암호화를 처리하는 EncryptedSharedPreferences 인스턴스가 필요합니다. SharedPreferences의 인스턴스는 Context#getSharedPreferences 또는 Activity#getPreferences 에서 직접 가져올 수 있지만 EncryptedSharedPreferences의 자체 인스턴스를 만들어야 합니다.

다음 예제를 참고하십시오.

val sharedPreferences = EncryptedSharedPreferences.create(
   “shared_preferences_filename”,
   masterKeyAlias,
   context,
   EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
   EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

SharedPreferences파일과 이전에 생성된 masterKeyAlias 그리고 Context의 이름을 전달해야 합니다. 마지막 두 인수는 각각 키와 값을 암호화합니다.  EncryptedSharedPreferences 인스턴스는 SharedPreferences 와 똑같이 사용하여 값을 읽을 수 있습니다.

val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
val sharedPreferences = EncryptedSharedPreferences.create(
   “shared_preferences_filename”,
   masterKeyAlias,
   context,
   EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
   EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// storing a value
sharedPreferences
   .edit()
   .putString(“some_key”, “some_data”)
   .apply()
// reading a value
sharedPreferences.getString(“some_key”, “some_default_value”) // -> “some_data”

Android Keystore

개발자는 Android Keystore를 사용하여 암호화 키를 디바이스에서 추출되지 않도록 보호할 수 있습니다. 또한 키를 사용하기 전에 사용자 인증을 요청하거나 특정 암호화 모드에서만 키 사용을 제한하는 등의 키 사용 방법과 시기를 제어하는 ​​메커니즘을 제공합니다. Android Keystore의 키를 보호하기 위해 다음의 보안 조치가 사용됩니다.

  • 키 자료는 애플리케이션 프로세스에 들어 가지 않습니다. 이렇게 하면 앱의 프로세스가 손상된 경우에도 해커가 키 자료를 추출할 수 없게 됩니다.
  • 키 자료는 장치의 TEE(Trusted Execution Environment) 또는 SE(Secure Element)와 같은 보안 하드웨어에 바인딩됩니다. 이 기능을 사용하면 보안 하드웨어 외부로 키 자료가 노출되지 않습니다. 따라서 공격자가 Keystore의 키를 사용할 수 있더라도 디바이스에서 추출할 수 없습니다. 이 기능을 확인하려면 키에 대한 KeyInfo를 얻고 KeyInfo.isInsideSecurityHardware()의 반환 값을 찾으십시오.

Android 디바이스에서 키의 무단 사용을 방지하기 위해 앱은 키를 생성하거나 가져오는 동안 승인된 사용을 지정할 수 있습니다. 키를 생성하거나 가져온 후에는 권한을 변경할 수 없습니다. Android Keystore에서 지원되는 키 사용 승인은 다음과 같이 분류할 수 있습니다.

  • 암호화 : 인증된 키 알고리즘, 블록 모드, 패딩 체계, 작업 또는 목적(암호화, 복호화, 서명, 확인) 및 키를 사용할 수 있는 다이제스트
  • 임시 유효성 간격 : 키 사용 권한이 부여된 시간 간격을 지정합니다.
  • 사용자 인증 : 사용자는 키를 사용할 수 있을 만큼 최근에 인증을 받아야 합니다.

Android OS에서 앱을 설치하려면 서명이 되어 있어야 합니다. 이것은 기본적으로 보안 인증서용 스토리지 시스템인 Keystore를 생성하여 수행할 수 있습니다. 구글 플레이 스토어와 같은 마켓플레이스에 앱을 배포하기 전에 공개키 인증서를 사용하여 APK에 서명해야 합니다. APK  서명을 통해서 이후 동일한 앱에 대한 업데이트가 악의적인 제3자가 아닌 개발자에 의해 수행됨을 보장할 수 있습니다. 동일한 앱이 향후 다른 키로 서명된 경우에 앱을 배포할 수 없기 때문에, 배포전에 앱의 수명을 고려해야 합니다. 앱의 수명이 결정되면 keytool 도구를 사용하여 Keystore를 생성할 수 있습니다.keytool은 Java JDK 설치 후에 확인할 수 있습니다. keytool 에는 여러 명령이 있으며 Android 앱에서 가장 일반적으로 사용되는 명령은 -genkeypair이며 축약형은 -genkey입니다. 일반적으로 사용되는 -genkey

옵션은 다음과 같습니다.

-alias               키쌍 별칭 이름
-keyalg               키쌍을 생성하는데 사용되는 알고리즘
-keysize               키쌍 크기(비트)
-validity               키쌍 유효 기간(일)

 

예를 들어, keytool -genkey -v -keystore release.keystore -alias example -keyalg RSA -keysize 2048 -validity 12000 명령은  release.keystore 이라는 별칭 이름과 12,000일의 유효 기간의 RSA-2048 공개/개인키 쌍이 있는 release.keystore라는 Keystore 파일을 생성합니다. APK에 서명하려면 Keystore와 키 모두에 대한 강력한 비밀번호가 필요합니다. APK는 다음 두 가지 방법으로 서명할 수 있습니다.

  • Gradle로 서명

gradle에서 프로젝트를 빌드하기 위해 개발자는 android.signingConfig를 만든 다음 하나 이상의 android.buildTypes과 연결할 수 있습니다. gradle 빌드 스크립트에 키 저장소 이름과 별칭을 입력하는 가장 간단한 방법은 다음과 같습니다.

android {
    signingConfigs {
        release {
            storeFile config.keystore
            storePassword storePassword
            keyAlias example
            keyPassword keyPassword
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
  }

  • 수동으로 서명

APK에 수동으로 서명하려면 build-tools 버전 24.0.3 이상의 경우 {ANDROID_SDK_DIRECTORY}/build-tools/{BUILD_TOOLS_VERSION}/apksigner 경로에 위치한 apksigner 를 사용하세요. 이 도구는 공개키 인증서를 생성하기 위해 Keystore에 저장된 공개/개인키 쌍을 사용합니다. 그런 다음 개인키가 유일한 방식으로 연결된 APK에 해당 인증서를 첨부합니다. 이후 앱의 압축되지 않은 데이터가 예측 가능한 오프셋에서 시작되도록 APK를 zipalign합니다. 물론 구글 플레이 스토어에는 zipalign APK를 필요로 합니다. 

zipalign -v -p 4 app-flavor-buildtype-unsigned.apk app-flavor-buildtype-unsigned-aligned.apk

  • APK가 압축된 후 apksigner를 사용하여 서명합니다.

apksigner sign –ks release.keystore –out app-flavor-buildtype.apk app-flavor-buildtype-unsigned-aligned.apk

  • 그런 다음 명령줄에서 Keystore의 비밀번호를 입력하라는 메시지가 표시됩니다. Keystore 및 키의 비밀번호는 독립적으로 입력해야 합니다. –ks-pass 및 –key-pass 옵션을 사용하여 개별적으로 비밀번호를 입력하라는 메시지를 표시할 수 있습니다. 각 옵션을 따라 stdin을 통해서 명령줄로부터 비밀번호를 캡처합니다.

apksigner sign –ks example.keystore –ks-pass stdin –key-pass stdin –out app-signed.apk app.apk

React Native API 보안 문제 조사

API는 주로 JSON 형식을 사용하며, 특정 엔드포인트가 있는 데이터 세트입니다. API에서 데이터에 액세스한다는 것은 API 프레임워크를 사용하여 특정 엔드포인트에 액세스하는 것을 의미합니다. React API는 애플리케이션과 다른 플랫폼 및 서비스 간의 통신을 설정하는데 사용됩니다. 또한 다른 디바이스나 앱이 설치된 특정 디바이스를 제어할 수 있는 옵션도 제공합니다. 이러한 API는 인증이 적절하지 않거나 비즈니스 로직에 결함이 있는 경우, 앱이 보안 위협 또는 MITM(man-in-the-middle) 공격에 취약하게 만들 수 있습니다. XSS 및 SQL 인젝션(SQLi) 공격도 가능합니다. React Native에서 API는 내부적으로 필요한 명령을 실행하기 위해 정보를 자동으로 문서화 합니다.

다음과 같은 방법으로 API 보안 관련 장애를 줄이거나 방지할 수 있습니다.

  • 각각의 API 스키마를 사용하여 모든 API 호출 명령의 유효성 검사
  • 악성코드 인젝션이나 보안 공격을 방지하기 위한 스키마의 주기적이고 시기 적절한 검증
  • SSL/TLS 피닝으로 애플리케이션의 안전함을 보장

DDoS 공격에 대한 React Native 보안

DDoS(분산 서비스 거부)는 권한이 없는 사용자가 특정 애플리케이션 서비스에 실제 사용자가 액세스할 수 없도록 만드는 일종의 악성 공격입니다. 이 취약점은 일반적으로 서비스의 IP가 제대로 마스킹되지 않았거나 애플리케이션이 충분히 안전하지 않은 경우 발생합니다. 이러한 공격은 클라이언트와 서버간의 통신을 중단시켜 앱의 온라인 서비스에 차질을 빚게 합니다. 또한 DDoS 공격이 기존 서비스를 중단시키는 대신 악의적인 트래픽으로 React 프로젝트를 플러딩시킬 수 있다는 것도 확인되었습니다. DDoS로 인해 일반적으로 발생하는 보안 공격은 다음과 같습니다.

  • UDP 플러딩 – 호스트 서비스에 액세스 할 수 없게됩니다.
  • ICMP 플러딩 – React 애플리케이션을 상당히 느리게 만듭니다.
  • SYN 플러딩 – 애플리케이션 서비스를 쉽게 공격할 수 있게 합니다. 
  • Ping of Death(POD) – 메모리 버퍼 오버플로우 발생시킵니다.
  • HTTP 플러딩 – 결국 애플리케이션 서비스의 완전한 종료를 야기하는 온라인 서비스의 중단을 초래합니다.

다음은 DDoS 공격을 처리할 수 있는 몇 가지 방법 입니다.

  • DDoS 위협을 식별하기 위해 개발 및 그 이후에 애플리케이션을 스크러빙
  • 악의적인 사용자가 프로그램 코드에 액세스하지 못하도록 방문자 식별 메커니즘 설치
  • 서버에서 호출하고 클라이언트 측에서는 호출하지 않음
  • CAPTCHA 또는 JS 테스트의 도움으로 웹-앱 계층 보안
  • 동일한 소스에서 IP에 대한 요청 수의 속도 제한

코드 난독화

코드 난독화 또는 최소화는 민감한 데이터를 저장하는 가장 기본적인 방법입니다. 읽을 수 있는 코드를 Uglify와 같은 소프트웨어를 사용하여 사람이 육안으로 읽을 수 없게 만듭니다. React Native의 Java 코드는 난독화되지 않는 한 읽을 수 있는 DEX 파일로 저장됩니다.

React Native에는 react-native-obfuscating-transformer로 알려진 난독화 라이브러리가 내장되어 있어 사용자가 JavaScript 코드와 모든 기본 코드에 대해 난독화를 구현할 수 있습니다.

첫 번째 단계는 애플리케이션의 gradle 파일에서 난독화를 활성화하는 것입니다. buildTypes 섹션 내에서 [app_name]/android/app/build.gradle로 이동하여 아래 코드를 적용할 수 있습니다.

android {
    // … other config
    buildTypes {
        release {
            debuggable false
            shrinkResources true
            zipAlignEnabled true
            minifyEnabled true
            useProguard true
            setProguardFiles([getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’])
        }
    }
    // … other config
}

minifyEnableduseProguard는 코드의 최소화 및 난독화를 활성화하는 반면 setProguardFiles 지시문은 proguard 구성의 위치를 나타냅니다. Proguard는 코드를 보호하는 동시에 불필요한 부분을 제거하고 최종 앱 크기를 줄이는 최적화를 수행합니다. proguard 구성은 코드를 난독화하는데 필요한 모든 예외가 추가되는  (proguard-rules.pro)파일로 설정할 수 있습니다.

 

React Native의 진보한 보안

앱이 여러 디바이스에서 실행되므로 네트워크 요청과 관련된 몇 가지 위협이 있습니다. 루팅되고 탈옥된 기기에서 Intent에 의해서 앱을 실행하는 것은 본질적으로 안전하지 않기 때문에 완전히 피해야 합니다. 루팅된 디바이스는 공격자가 OS의 보안 메커니즘을 무력화시키는데 도움을 주며, 보안 스토리지 및 데이터에 쉽게 접근할 수 있도록 합니다. JailMonkey는 애플리케이션이 디바이스의 루팅 또는 탈옥 여부를 감지하는데 사용할 수 있습니다. Android 전용 API인 SafetyNet은 루팅된 디바이스 및 부트 로더의 잠금 해제를 감지하는데 도움이 됩니다. 또한 보안 위협, 디바이스 변조, 악성 앱 및 가짜 사용자에 대한 정보를 제공합니다. SafetyNet의 API용 래퍼 플러그인인 react-native-google-safetynet을 사용하여 사용자의 디바이스를 검증할 수 있습니다. 또한 react-native-device-info 플러그인을 사용하여 앱이 에뮬레이터에서 실행 중인지 확인할 수 있습니다.

RASP

런타임 애플리케이션 자가 보호(RASP) 도구는 애플리케이션 스토리지에 대한 공격을 지속적으로 감지하고 앱을 보호할 수 있습니다. 이 도구는 앱의 런타임 환경내에 구축되며 앱의 런타임 실행을 제어하여 앱의 성능과 동작을 분석할 수 있습니다. 추가 보안 계층을 제공하고 다른 보안 및 앱 모니터링 도구와 함께 작동합니다. 앱이 RASP 기능을 갖기 위해서는 앱 런타임 실행을 제어하고, 앱 성능 및 동작을 모니터링하고, 침입 또는 비정상 동작을 감지하여야 합니다.

결론

React Native는 가장 인기 있고 효율적인 앱 구축 프레임워크 중 하나입니다. 크로스 플랫폼 지원, 사용 용이성, 비용 및 리소스 최적화, 뛰어난 사용자 인터페이스는 전세계 개발자에게 훌륭한 선택입니다. 사전에 만들어진 컴포넌트 및 내장 라이브러리를 풍부하게 사용할 수 있으므로 간단한 기능을 쉽게 수행하는데 적합합니다. 그러나 프레임워크가 JavaScript 코드를 기반으로 만들어 졌기  때문에 이를 사용한 앱은 보안 위협과 외부의 악의적인 침입 시도에 늘 취약합니다. 또한 앱 전체에 걸쳐 유사한 구성 요소를 사용하면, 아무래도 보안 위험도 높아집니다. 따라서 개발자는 Android 및 iOS에서 사용할 수 있는 모든 보안 기능을 사용하여 React Native 애플리케이션을 보호해야 합니다. 앱의 사용 사례와 사양을 바탕으로 위협 모델을 생성한 후 앱 보안에 대한 공격을 방지하기 위해 필요한 예방 조치를 취하는 것이 중요합니다. 100% 보안을 보장하는 방탄 메커니즘은 없지만 적절한 라이브러리와 API를 통합하면 위험 발생을 확실히 줄일 수 있습니다.

추가 코딩없이 애플리케이션을 보호하려면 아래 링크를 클릭하여 앱실링에 대해 자세히 알아보고 무료 평가판을 사용해 보십시오.

앱 보안하러 가기

Dustin Hong
Dustin Hong
Dustin은 잉카엔트웍스의 앱실링 비즈니스 개발을 이끌고 있습니다. 그는 사이버 보안, IT, 컨텐츠 및 애플리케이션 보안 분야의 소프트웨어 개발과 혁신에 많은 관심을 가지고 있습니다. 또한 사이버 보안 세계에서 주요 사건의 대상, 이유 및 방법에 대해 다양한 사람들에게 공유하고 토론하는 것을 좋아합니다. 업계 동향 및 모범 사례에 대한 그의 견해는 기사, 백서에 실려있으며, 여러 보안 행사에서 유사 주제로 발표를 하였습니다.

Leave a Comment