Android

[Java][Android] SharedPreferences 데이터 암호화 하기!

어렵지만 2025. 5. 26. 14:30

내 앱의 비밀번호, SharedPreferences에 그냥 저장해도 될까? 🤫 

EncryptedSharedPreferences로 안전하게!


안녕하세요, 안드로이드 개발자 여러분! 앱 설정을 저장하거나 간단한 사용자 데이터를 유지할 때 SharedPreferences는 정말 편리한 도구입니다. 하지만 여기에 사용자 ID, 토큰, 심지어 (절대 그러면 안 되지만) 비밀번호 같은 민감한 정보를 그대로 저장하고 계신가요? 😱 

 

루팅된 기기에서는 SharedPreferences 파일에 쉽게 접근하여 내용을 볼 수 있다는 사실, 알고 계셨나요?
오늘은 이러한 보안 문제를 해결하고, SharedPreferences에 저장되는 데이터를 안전하게 암호화하는 방법, 바로 **EncryptedSharedPreferences**에 대해 알아보겠습니다.


😢 일반 SharedPreferences의 보안 취약점
기본 SharedPreferences는 데이터를 일반 텍스트(Plain Text) 형태로 XML 파일에 저장합니다. 이 파일은 앱의 내부 저장소 (/data/data/<패키지명>/shared_prefs/)에 위치하며, 루팅되지 않은 일반 기기에서는 다른 앱이나 사용자가 직접 접근하기 어렵습니다.
하지만, 만약 기기가 루팅(Rooting)되어 있다면 상황은 달라집니다. 루팅된 기기에서는 관리자 권한을 획득하여 이 파일에 자유롭게 접근하고 내용을 열람하거나 수정할 수 있습니다. 이는 API 키, 인증 토큰, 개인 식별 정보 등 민감한 데이터가 노출될 심각한 위험을 초래합니다.

 

Jetpack Security의 EncryptedSharedPreferences
다행히 Google에서는 이러한 문제를 해결하기 위해 Jetpack Security ( androidx.security ) 라이브러리를 제공하며, 그 안에 EncryptedSharedPreferences라는 도구를 포함하고 있습니다.
EncryptedSharedPreferences는 기존 SharedPreferences와 거의 동일한 API를 제공하면서, 저장되는 키(key)와 값(value)을 모두 암호화합니다. 이를 통해 루팅된 기기에서 파일에 직접 접근하더라도 암호화된 내용만 보이므로, 원래 데이터를 알아내기 어렵게 만듭니다.


주요 특징:
자동 암호화/복호화: 데이터를 저장할 때 자동으로 암호화하고, 읽을 때 자동으로 복호화합니다. 개발자는 암호화 과정을 신경 쓸 필요 없이 기존 SharedPreferences처럼 사용하면 됩니다.


암호화 알고리즘: AES-256 GCM과 같은 암호화 표준을 사용합니다.


마스터 키 관리: 암호화에 사용되는 마스터 키를 Android Keystore 시스템을 통해 안전하게 생성하고 관리합니다. Android Keystore는 하드웨어 지원 보안 기능을 활용하여 키를 기기 내에 안전하게 보관합니다.

 

예제코드

dependencies {
    implementation libs.security.crypto // 최신 안정 버전 확인 권장
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editTextUsername"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="사용자 이름 입력"
        android:inputType="textPersonName"
        android:layout_marginBottom="16dp"/>

    <androidx.appcompat.widget.SwitchCompat
        android:id="@+id/switchDarkMode"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="다크 모드 활성화"
        android:textSize="16sp"
        android:layout_marginBottom="24dp"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginBottom="24dp">

        <Button
            android:id="@+id/buttonSave"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="저장하기"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/buttonLoad"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="불러오기"/>
    </LinearLayout>

    <TextView
        android:id="@+id/textViewLoadedData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="불러온 데이터가 여기에 표시됩니다."
        android:textSize="16sp"
        android:padding="8dp"
        android:background="#f0f0f0"/>

</LinearLayout>
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;

import java.io.IOException;
import java.security.GeneralSecurityException;
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey; // MasterKey.Builder를 사용
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static final String KEY_USERNAME = "username";
    private static final String KEY_DARK_MODE = "dark_mode_enabled";

     EditText editTextUsername;
     SwitchCompat switchDarkMode; // <--- 타입을 SwitchCompat으로 변경!
     Button buttonSave, buttonLoad;
     TextView textViewLoadedData;
     SharedPreferences encryptedPrefs;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); // 레이아웃 파일 설정 필요

        editTextUsername = findViewById(R.id.editTextUsername);
        switchDarkMode = findViewById(R.id.switchDarkMode);
        buttonSave = findViewById(R.id.buttonSave);
        buttonLoad = findViewById(R.id.buttonLoad);
        textViewLoadedData = findViewById(R.id.textViewLoadedData);

        // EncryptedSharedPreferences 초기화
        try {
            encryptedPrefs = getEncryptedSharedPreferences();
        } catch (RuntimeException e) {
            // 오류 발생 시 사용자에게 알리거나 대체 로직 수행
            Toast.makeText(this, "보안 저장소 초기화 실패!", Toast.LENGTH_LONG).show();
            Log.e(TAG, "EncryptedSharedPreferences 초기화에 실패했습니다.", e);
            // 일반 SharedPreferences로 대체할 수도 있습니다.
            // encryptedPrefs = getSharedPreferences("app_prefs_fallback", MODE_PRIVATE);
            finish(); // 또는 앱 종료
            return;
        }


        buttonSave.setOnClickListener(v -> saveData());
        buttonLoad.setOnClickListener(v -> loadData());

        // 앱 시작 시 저장된 데이터 자동 로드 (선택 사항)
        loadData();
    }

    private SharedPreferences getEncryptedSharedPreferences() {
        try {
            // 1. MasterKey.Builder를 사용하여 마스터 키 생성
            MasterKey mainKey = new MasterKey.Builder(getApplicationContext())
                    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) // 권장되는 키 스킴
                    .build();

            // 2. EncryptedSharedPreferences 인스턴스 생성
            return EncryptedSharedPreferences.create(
                    getApplicationContext(),
                    "secure_app_prefs_v2", // 파일 이름
                    mainKey, // 생성된 MasterKey 객체
                    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, // 키 암호화 방식
                    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM  // 값 암호화 방식
            );
        } catch (GeneralSecurityException | IOException e) {
            Log.e("EncryptedPrefs", "EncryptedSharedPreferences를 생성하지 못했습니다.", e);
            throw new RuntimeException("Failed to create EncryptedSharedPreferences", e);
        }
    }

    private void saveData() {
        if (encryptedPrefs == null) return;

        String username = editTextUsername.getText().toString();
        boolean darkModeEnabled = switchDarkMode.isChecked();

        SharedPreferences.Editor editor = encryptedPrefs.edit();
        editor.putString(KEY_USERNAME, username);
        editor.putBoolean(KEY_DARK_MODE, darkModeEnabled);
        editor.apply(); // 비동기 저장 (또는 editor.commit()으로 동기 저장)

        Toast.makeText(this, "데이터가 안전하게 저장되었습니다.", Toast.LENGTH_SHORT).show();
        Log.d(TAG, "저장된 데이터: 사용자 이름=" + username + ", 다크모드=" + darkModeEnabled);
    }

    private void loadData() {
        if (encryptedPrefs == null) return;

        String username = encryptedPrefs.getString(KEY_USERNAME, "사용자 이름 없음");
        boolean darkModeEnabled = encryptedPrefs.getBoolean(KEY_DARK_MODE, false);

        editTextUsername.setText(username); // 불러온 데이터로 UI 업데이트 (선택적)
        switchDarkMode.setChecked(darkModeEnabled);

        String loadedText = "불러온 데이터:\n사용자 이름: " + username + "\n 다크 모드: " + (darkModeEnabled ? "활성화됨" : "비활성화됨");
        textViewLoadedData.setText(loadedText);

        Log.d(TAG, "불러온 데이터:\n사용자 이름:" + username + ", \n 다크 모드=" + darkModeEnabled);
    }
}

 

시연영상