안드로이드 자바

[Java][Android] SharedPreferences 데이터 암호화

teamnova 2025. 5. 26. 14:26
728x90

SharedPreferences의 보안 취약점
기본 SharedPreferences는 데이터를 일반 텍스트(Plain Text) 형태로 XML 파일에 저장합니다. 이 파일은 앱의 내부 저장소 (/data/data/<패키지명>/shared_prefs/)에 위치하며, 루팅되지 않은 일반 기기에서는 다른 앱이나 사용자가 직접 접근하기 어렵습니다.

 

주요 특징:
자동 암호화/복호화: 데이터를 저장할 때 자동으로 암호화하고, 읽을 때 자동으로 복호화합니다. 개발자는 암호화 과정을 신경 쓸 

필요 없이 기존 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 = "불러온 데이터:사용자 이름: " + username + "다크 모드: " + (darkModeEnabled ? "활성화됨" : "비활성화됨");
        textViewLoadedData.setText(loadedText);

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

시연영상