본문 바로가기

Android 하나하나 집어보기

RealmDB와 AES 암호화를 이용하여 데이터 저장하기

RealmDB 란??


데이터베이스다. 하지만 기존 Sqlite 보다 좀더 사용성이 쉽고 빠른 데이터라고 생각하면 된다. 몇가지 로컬데이터를 다루는 라이브러리들이 있지만 개인적으로 가장 쉽게 사용 가능한 데이터라고 생각하기에 이번 포스팅에선 Realm 데이터베이스를 다뤄보려고 한다. 속도 관련해선 아래 그림을 참조해보면 감이올 것이다.

(사진출처 :https://academy.realm.io/kr/posts/realm-object-centric-present-day-database-mobile-applications/)


Realm 의 안드로이드에서 가장 큰장점은 클래스로 쉽게 사용이 가능하다는것이다. 여타 다른 수많은 장점들이 있다 객체로서,암호화,실시간반응형 등등 많지만 필자는 클래스를 이용하여 데이터 매핑을 한다는점이 상당히 맘에 들었다. 안드로이드에서 사용자 정보를 로컬에 저장해본다고 가정해보자.


(사진출처 : 직접제작)


사실 패스워드나 개인정보는 저장하지않고 토큰방식을 쓰는것을 추천하지만 예시로서만 보는것이 좋다. 일단 우린 UserId 와 PassWord를 저장할것이고 이 데이터를 저장할때 aes 양방향 암호화 방식을 통해서 저장할것이다. 다만 기존에 Realm데이터베이스도 AES 암호화 방식을 configuration 를 통해서 제공하지만 단하나의 키로 모든걸 암호화하기 때문에 필자는 각데이터마다 새로다른 키값을 데이터에 넣어줄것이고 하나의 키값은 하나의 데이터만 복호화할수 있도록 할것이다. 이것도 완전히 안전한 방법은 아니지만 그래도 기존것보단 좀 더 안전하다. 


일단 저 두데이터를 어떻게 Realm에서 이용하는지 먼저 살펴보자 먼저 두가지 클래스를 만들어준다.

UserData Class

public class UserData extends RealmObject {
String userId;
String password;
long keyId;

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public long getKeyId() {
return keyId;
}

public void setKeyId(long keyId) {
this.keyId = keyId;
}
}


KeyData Class


public class KeyData extends RealmObject{
String aesKey;

@PrimaryKey
long id;

public String getAesKey() {
return aesKey;
}

public void setAesKey(String aesKey) {
this.aesKey = aesKey;
}

public long getId() {
return id;
}
}


두 클래스를 보면 딱 감이오겠지만 gson 매핑과 상당히 유사하다. 각각의 들어갈 데이터의 칼럼네임을 변수로서 세팅하고 RealmObject를 상속받으면 끝이다. 다만 KeyData에서 PrimaryKey 같은 어노테이션을 이용하여 데이터베이스의 프라이머리 키값을 세팅할 수 있다.


이제 유저정보를 입력할 화면을 만들어보자

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="com.edge.realmstudy.MainActivity">

<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="50dp"
android:id="@+id/toolbar"
android:background="@android:color/black"
app:contentInsetStart="0dp"
android:elevation="5dp"
app:layout_constraintTop_toTopOf="parent">
<RelativeLayout
android:layout_width="match_parent"
android:background="@android:color/white"
android:layout_height="50dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"
android:text="회원가입"
android:layout_centerInParent="true"/>

</RelativeLayout>
</android.support.v7.widget.Toolbar>
<EditText
android:id="@+id/email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:ems="10"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"/>

<EditText
android:id="@+id/pwd"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:layout_marginTop="8dp"
android:ems="10"
android:inputType="textPassword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView2"
app:layout_constraintStart_toStartOf="@+id/pwd"
android:layout_marginTop="50dp"
android:textColor="@android:color/holo_orange_dark"
android:text="Password"
app:layout_constraintTop_toBottomOf="@+id/email"/>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Email"
android:textColor="@android:color/holo_orange_dark"
app:layout_constraintStart_toStartOf="@+id/email"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
android:layout_marginTop="40dp"/>

<Button
android:id="@+id/register"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="100dp"
android:text="가입하기"
android:textColor="@android:color/white"
android:background="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent" />
</android.support.constraint.ConstraintLayout>

귀찮으면 위코드 복사 ㅋㅋㅋ


그리고 MainActivity로 이동하여서 RealmDB를 사용해보도록 하겠다.

private void initView() {
email = findViewById(R.id.email);
password = findViewById(R.id.pwd);
register = findViewById(R.id.register);
register.setOnClickListener(this);
}


일단 가볍게 intiView() 메소드를 만들어서 위젯을 초기화 해준다. 다음으로 아래와 같이 Realm를 초기화를 해준다.

private void realmInit() {
Realm.init(getApplicationContext());
RealmConfiguration configuration = new RealmConfiguration.Builder().deleteRealmIfMigrationNeeded().build();
realm = Realm.getInstance(configuration);
}

위와 같이 Realm 을 초기화가 가능한데 메소드 두번째줄을 보면 configuration을 통하여 여러가지 realmdb를 세팅할수 있다. 위코드는 realm데이터에 byte[]형태의 데이터도 저장가능하도록 세팅한 속성이다. 기존 sqlite와 비슷한 속성을 가졌다고 보면된다.


다음으로 우린 Edittext에서 받은 데이터를 Real에 넣어주기 직전에 먼저 AES 암호화를 위한 KEY데이터부터 저장할것이다. 그러기 위해선 aes 암호화가 어떤것인지 대략 살펴보고 넘어가자.


public class AESUtils {

//암호화
public static String encrypt(String str, String secretKey) throws Exception {
byte[] keyData = secretKey.getBytes();
SecretKey secureKey = new SecretKeySpec(keyData, "AES");
String IV = secretKey.substring(0, 16);
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
c.init(Cipher.ENCRYPT_MODE, secureKey, new IvParameterSpec(IV.getBytes()));
byte[] encrypted = c.doFinal(str.getBytes("UTF-8"));
String enStr = new String(Base64.encode(encrypted, Base64.DEFAULT));
return enStr;
}

//복호화
public static String decrypt(String str, String secretKey) throws Exception {
byte[] keyData = secretKey.getBytes();
String IV = secretKey.substring(0, 16);
SecretKey secureKey = new SecretKeySpec(keyData, "AES");
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
c.init(Cipher.DECRYPT_MODE, secureKey, new IvParameterSpec(IV.getBytes("UTF-8")));
byte[] byteStr = Base64.decode(str.getBytes(), Base64.DEFAULT);
return new String(c.doFinal(byteStr), "UTF-8");
}

//키생성
public static String getRandomString() {
String SALTCHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
StringBuilder salt = new StringBuilder();
Random rnd = new Random();
while (salt.length() < 32) { // length of the random string.
int index = (int) (rnd.nextFloat() * SALTCHARS.length());
salt.append(SALTCHARS.charAt(index));
}
String saltStr = salt.toString();
return saltStr;
}
}

AES 256은  키값을 통해서 데이터를 암호화 혹은 암호화된 데이터를 다시 복호화 할 수 있는 양방향 암호화 모듈이다. RSA 암호화처럼 공개키 개인키 개념이 없이 하나의 키를 통해 암호화 복호화를 실행한다. 그렇기 때문에 KEY 데이터 관리가 상당히 중요하다. 그래서 우린 KEY 값을 안드로이드의 상수 코드로 넣는것이 아닌 랜덤 키값을 생성해서 바로 데이터베이스에 저장하고 그것을 열람하여 사용하는 형식으로 진행할 것이다.


안드로이드에 직접코드로 AES 키값을 넣는 행위는 암호화를 안하겠다는 것과 마찬가지다. (이유는 안드로이드는 소스코드 까보는것이 너무쉽고 string 상수코드는 일반적인 프로가드로는 난독화가 되지않는다.) 

AES암호화는 상대적으로 아직 안전하다고는 하나 키값 관리가 제대로안되면 치명적인 암호화모듈인것은 양방향 암호화의 어쩔수 없는 단점이다.


여기까지 대략 AES 암호화 클래스를 살펴봤고 저코드를 프로젝트에 복사해 넣어두도록하자. 

카피엔 페이스트는 언제나 승리할것이다.!!!!


다음으로 앞서말한 keydata를 realm 데이터에 넣어보자.

private void generateKeyData() {
try { realm.beginTransaction();
aesKey = AESUtils.getRandomString();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Number max = realm.where(KeyData.class).max("id");
int nextId = (max == null) ? 0 : max.intValue() + 1;
KeyData keySaltPairData = realm.createObject(KeyData.class, nextId);
keySaltPairData.setAesKey(aesKey);
keyId = nextId;
realm.insertOrUpdate(keySaltPairData);
realm.commitTransaction();

}
});

} catch (Exception e) {
e.printStackTrace();
}
}

코드가 생각보다 길다. 하지만 천천히 살펴보자.  AESUtils 클래스에서 랜덤한 키값을 가져와서 realm데이터에 executeTransaction을 통하여 데이터를 넣어줄 것이다. execute 안에 코드를 살펴보면 필자는 primarykey를 index형태로 넣어줄 예정이라 realm데이터에서 .where(T class) 를 통한 데이터를 불러오고 .max()를 통해서 데이터가 몇개 있는지를 Number 객체로 받아준다. 그리고나서  null 일때를 판별해서 0으로 최초값을 넣고 아니라면 +1 을 시키는형식으로 해서 다음 키id값을 변수로 받아둔다. 그후  넣어줄 데이터를 세팅하고 데이터를 넣어준다.

키ID값을 변수로 받아둔 이유는 이후에 유저데이터 등록할때 keyId 가 필요하기 때문이다.


다음으론 클릭이벤트쪽 코드를 보겠다.

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.register:
String id = email.getText().toString();
String pwd = password.getText().toString();
if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(pwd)) {
try {
registerUser(AESUtils.encrypt(id,aesKey), AESUtils.encrypt(pwd,aesKey),keyId);
} catch (Exception e){
e.printStackTrace();
}
}
break;
}
}

별거없다. 아이디와 패스워드를 받아두고 null 체킹을한뒤 registerUser 라는 다음에 만들 함수의 인자로 각각 데이터를 넘겨주는데 이때 암호화를 해서 데이터를 넘겨주고 방금 위에서 만들었던 keyId 도 인자로 넘겨준다.


아래 registerUser 함수를 보자. realm.beginTransaction()을 통해서 realm의 시작을 알리고 데이터를 넣어준뒤 intent를 통해서 다음 액티비티로 넘긴다. 주의할 점은 realm 이 이미 시작되었는데 다시 begin 메소드를 호출하면 오류가나기때문에 if 문으로 걸러준다.

private void registerUser(String  userId, String  pwd,long keyId) {
if (!realm.isInTransaction()){
realm.beginTransaction();
}
UserData userData = realm.createObject(UserData.class);
userData.setUserId(userId);
userData.setPassword(pwd);
userData.setKeyId(keyId);
realm.commitTransaction();
Intent intent =new Intent(this,Main2Activity.class);
startActivity(intent);
}

이얼마나 간단한가. sqlite 쿼리문 짯던 우리의 슬픈기억을 떠올려보자. ㅠㅠ 

마지막으로 oncreate될때 혹시나 이부분 놓치면 섭섭해 하시는분들을 위해서 올려놓는다.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
realmInit();
generateKeyPair();
}


지금 까지 전체로직은 이러하다. 회원가입부분에서 oncreate를 통할때 realm 초기화를 하고 keyData를 세팅한뒤 Edittext에서 데이터를 받아와서 realm 에 암호화를 하여 넣어주는 것이다. 어렵지 않고 천천히 해보면 금방 따라 할 수 있을것이다.

그리고 액티비티가 죽었을때 realm 을 사용하지 않을때는 꼭

@Override
protected void onDestroy() {
super.onDestroy();
if (realm != null && !realm.isClosed()) {
realm.close();
}
}

이런식으로 닫아준다.


마지막으로 데이터를 복호하화는부분을 보면

private void logUserInfo() {
String pwd = null;
String userId = null;
try {
userId = AESUtils.decrypt( data.getUserId(),aesKey);
pwd = AESUtils.decrypt(data.getPassword(),aesKey);
} catch (Exception e) {
e.printStackTrace();
}
}

위와 같이 간단히 사용 가능하다. 솔직히 말해 AESUtils 클래스 만드느라 힘들었다. 패딩이니뭐니 세팅이 너무달라서 오류가 많았다. 

이상 간단한 회원가입 데이터 저장 부분을 진행해봤지만 절대 이대로하는걸 추천하지 않는다. 유저 정보는 꼭 서버에 저장하고 토큰을 받아서 로그인 처리를 하길 바란다. 


이상 오늘 포스팅은 여기까지!!


ps.


비전공자 안드로이드 질문방을 운영중입니다. 

톡방링크 (링크) 를 통해 들어오시면 못다 설명드린내용들 자세히 설명드릴게요!!! 

이깟 블로그보다 직접만나서 배워보고 싶으시면 말리지 않습니다. 어서오세요 (링크)

마지막으로....제가 만든 앱 (링크) 입니다. 리뷰... 하나가 생명을 살립니다. 감사합니다.