지금 개발하고 있는 Touch me는 이벤트 광고 플랫폼으로 앱 사용자가 특정 광고에 참여 또는 공유하는 경우 일정 포인트가 쌓이게 된다. 참여 또는 공유 정보는 서버로 전송되고 유효한 액션인 경우에 포인트가 쌓이도록 해야하는데,

포인트는 현금과 1:1의 가치를 갖기 때문에 보안이 중요하다.

헌데 지금까지 보안을 고려한 개발을 제대로 해본적이 없어서, 어떻게 보호를 해야하고, 무엇을 보호해야할 지에 대해 모르는 상태다. 이 때문에 기본적인 암호화에 대한 공부를 하게 되었다.

지금까지는 인터넷에서 찾은 정보로 알게된 것이 전부이고, 더 공부를 하고 싶어서 '알아야 막는다 - 자바 JSP 해킹과 보안'과 '실용 암호학 : 보안 실무자를 위한 정보보호와 암호화 구현' 두 권의 책을 구매한 상태이다.

4학년 수업 중에 교재로 사용하는 정보 보호와 보안 이라는 책이 있었던 것 같은데 정확한 책 제목과 저자를 알지 못해 함께 구매하진 못했다.


앱을 통해 포인트를 적립하라는 HTTP 메시지의 암호화를 위해서는 무엇을 암호화 해야할까?

굳이 포인트 적립에 대한 메시지를 암호화하는 까닭은 현금과 포인트는 같은 가치를 가지기 때문에 유효하지 않은 포인트 적립, 즉 포인트 적립을 비정상적인 방법으로 적립할 수 있기 때문에 포인트 적립 메시지의 암호화를 총해 유효함을 검증하기 위함이다. 또한 비정상적인 광고 참여, 공유는 광고 효과와 서비스의 신뢰성과도 직결되기 때문에 서비스에서 중요한 부분일 수 있다.

사용자에게 광고 효과를 얻기 위해서 참여와 공유를 하도록 하고 이에 대한 보상으로 포인트를 지급하는 것인데, 광고 효과가 없는 포인트 지급은 의미 없는 지출일 뿐이니 말이다.


포인트 적립에 필요한 정보는 관련 광고, 유저 정보, 포인트 정보이다.

포인트 적립이 '유효함'을 보장하기 위해서는 사용자가 직접 어떤 액션을 해야한다는 것인데,

쓰다보니 굳이 암호화 하지 않더라도 서버에서 유효함을 보장하기 위한 1회성 토큰을 전송하는 것도 괜찮을 듯하다..


이 부분에 대해서는 아직 정리되지 않은 부분이 많아, 프로토콜에 대한 내용을 다시금 정리해봐야겠다.


서론이 너무 길었던 것 같다. 어쨌든.. 위의 고민을 하면서 알아봤던 RSA 암호화 알고리즘에 대해서 포스팅 해야겠다.

다시 잡설이지만 무엇인가를 배운 후에, 정리하거나 누군가에게 설명하는 일은 내가 다시 공부하는 것이라고 생각한다.


RSA 비대칭키 암호화 알고리즘

RSA 알고리즘은 공개키 암호 알고리즘의 하나로 이를 연구하였던 Ron Rivest, Adi Shamir, Leonard Adleman의 이름 첫글자를 따서 지어진 이름이다.

한국 위키피디아의 RSA 암호 문서의 내용에 따르면 RSA 암호 체계의 안정성은 큰 숫자를 소인수분해하는 것이 어렵다는 것에 기반을 두고 있다고 한다. 허나 1993년 피터 쇼어가 쇼어 알고리즘을 발표하면서 양자 컴퓨터를 이용하여 임의의 정수를 다항 시간 안에 소인수분해하는 방법을 발표하였다. 즉 양자 컴퓨터가 활성화된다면 RSA 알고리즘은 쉽게 깨지게 된다. 암호로써의 역할을 잃게 되는 것이다.

RSA 비대칭키 암호화 알고리즘은 위키피디아 문서에 잘 정리되어 있다.



비대칭키 암호화 알고리즘

암호화 알고리즘에는 대칭키 알고리즘과 비대칭키 알고리즘이 있다. 먼저 대칭키 알고리즘은 암호화/복호화에 사용되는 키가 동일한 알고리즘을 의미하고, 비대칭키 암호화 알고리즘은 암호화/복호화에 사용되는 키가 서로 다른 것을 의미한다. 여기서 암호화에 사용되는 키를 공개키(Public Key)라고 하고, 복호화에 사용되는 키를 비밀키(Private Key)라고 한다. RSA 알고리즘은 공개키, 비밀키 두개의 키를 생성하지만 암호화/복호화에 있어서 어느 하나만을 사용해야한다는 제약은 없다. 즉 암호화 시에 공개키를 사용하고 비밀키를 통해서 복호화를 할 수 있지만,  반대로 암호화 시에 비밀키를 사용하고 공개키를 통해서 복호화할 수도 있다.


일반적으로 대칭키 알고리즘이 비대칭키 알고리즘에 비해서 성능이 좋다. 따라서 대량의 데이터를 암호화 하기 위해서는 비대칭키 알고리즘의 성능은 떨어지게 된다. 성능상의 보정을 위해서 데이터는 대칭키 알고리즘으로 암호화 하고, 대칭키의 키만 비대칭키 알고리즘으로 암호화하는 방법을 사용하기도 한다.



자바에서 RSA 알고리즘의 사용

자바에서는 java.crypto 패키지 내에 암호화와 관련한 구현이 되어있다. Touch Me 에서 RSA 알고리즘 사용을 위해 간략하게 유틸리티 클래스를 작성해봤다.

주요 사용 클래스는 java.crypto 패키지의 Cipher, java.security 패키지의 KeyFactory, KeyPair, KeyPairGenerator, PrivateKey, PublicKey, RSAPrivateKeySpec, RSAPublicKeySpec이다.

package com.touchme.web.security;

import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.apache.commons.codec.binary.Base64;

/**
 * RSA 비대칭키 암호화 알고리즘 사용을 위한 유틸리티 클래스
 * @author 이준영
 *
 */
public class RSAUtils {
	public static final String RSA = "RSA";
	public static KeyPair generateKeyPair() {
		KeyPair keyPair = null;
		try {
			keyPair = KeyPairGenerator.getInstance(RSA).generateKeyPair();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return keyPair;
	}
	
	/**
	 * Public Key로 암호화한 후 결과로 출력된 byte 배열을 Base64로 인코딩하여 String으로 변환하여 리턴함
	 * @param text 암호화할 텍스트
	 * @param publicKey RSA 공개키
	 * @return Base64로 인코딩된 암호화 문자열
	 */
	public static String encrypt(String text, PublicKey publicKey) {
		byte[] bytes = text.getBytes();
		String encryptedText = null;
		try {
			Cipher cipher = Cipher.getInstance(RSA);
			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
			encryptedText = new String(Base64.encodeBase64(cipher.doFinal(bytes)));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		}
		
		return encryptedText;
	}
	
	/**
	 * Base64로 인코딩된 문자열을 받아 decode 시킨 후 RSA 비밀키(Private Key)를 이용하여 암호화된 텍스트를 원문으로 복호화
	 * @param encryptedBase64Text Base64로 인코딩된 암호화 문자열
	 * @param privateKey RSA 비밀 키
	 * @return 복호화된 텍스트
	 */
	public static String decrypt(String encryptedBase64Text, PrivateKey privateKey) {
		byte[] bytes = Base64.decodeBase64(encryptedBase64Text.getBytes());
		String decryptedText = null;
		try {
			Cipher cipher = Cipher.getInstance(RSA);
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			decryptedText = new String(cipher.doFinal(bytes));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		}
		
		return decryptedText;
	}
	
	/**
	 * RSA 공개키로부터 RSAPublicKeySpec 객체를 생성함
	 * @param publicKey 공개키
	 * @return RSAPublicKeySpec
	 */
	public static RSAPublicKeySpec getRSAPublicKeySpec(PublicKey publicKey) {
		RSAPublicKeySpec spec = null;
		try {
			spec = KeyFactory.getInstance(RSA).getKeySpec(publicKey, RSAPublicKeySpec.class);
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return spec;
	}
	
	/**
	 * RSA 비밀키로부터 RSAPrivateKeySpec 객체를 생성함
	 * @param privateKey 비밀키
	 * @return RSAPrivateKeySpec
	 */
	public static RSAPrivateKeySpec getRSAPrivateKeySpec(PrivateKey privateKey) {
		RSAPrivateKeySpec spec = null;
		
		try {
			spec = KeyFactory.getInstance(RSA).getKeySpec(privateKey, RSAPrivateKeySpec.class);
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return spec;
	}
	
	/**
	 * Moduls, Exponent 값을 이용하여 PublicKey 객체를 생성함
	 * @param modulus RSA Public Key Modulus
	 * @param exponent RSA Public Key exponent
	 * @return PublicKey 객체
	 */
	public static PublicKey getPublicKey(String modulus, String exponent) {
		BigInteger modulus_ = new BigInteger(modulus);
		BigInteger exponent_ = new BigInteger(exponent);
		PublicKey publicKey = null;
		
		try {
			publicKey = KeyFactory
					.getInstance(RSA)
					.generatePublic(new RSAPublicKeySpec(modulus_, exponent_));
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return publicKey;
	}
	
	/**
	 * Modulus, Exponent 값을 이용하여 PrivateKey 객체를 생성함
	 * @param modulus RSA private key Modulus
	 * @param privateExponent RSA private key exponent
	 * @return PrivateKey 객체
	 */
	public static PrivateKey getPrivateKey(String modulus, String privateExponent) {
		BigInteger modulus_ = new BigInteger(modulus);
		BigInteger privateExponent_ = new BigInteger(privateExponent);
		PrivateKey privateKey = null;
		
		try {
			privateKey = KeyFactory
					.getInstance(RSA)
					.generatePrivate(new RSAPrivateKeySpec(modulus_, privateExponent_));
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return privateKey;
	}
}


Base64

Touch Me에서는 문자열로 서버와 안드로이드 애플리케이션 간에 데이터를 주고받게 된다.

만약 JSON 포맷의 데이터라면 UTF-8로 인코딩된 문자열을 주고 받으므로 사람의 눈으로 문자를 확인하면 정상적인 JSON 문자열을 확인할 수 있다. 이 JSON 문자열을 암호화 하게 된다면 문자 인코딩이 적용되지 않은 바이트배열로 구성되어 있으므로 이를 문자열로 변환하여 사람의 눈으로 확인한다면 의미없는 문자 또는 폰트로 표현되지 않는 문자로 보여질 수 있다. 이 때 정상적인 문자로 보여주기 위해서 Base64로 인코딩한다.

간단하게 말하자면 Base64는 64진수를 의미한다. 6bit의 이진 데이터를 ASCII 문자로 매핑하여 변환하는데 숫자 0~1, a~z, A~Z, +, / 로 표현하게 된다. 이진 데이터가 6bit로 딱 떨어지지 않는 경우가 발생할 수 있다. 이를 표현하기 위해서 패딩(Padding) 문자 = 를 사용한다.

이진데이터의 길이 n (1bit를 1로 했을 때) 을 3으로 나눈 나머지 즉 r = n % 3 에서

r이 0인 경우 패딩문자를 붙이지 않는다.

r이 1인 경우 패딩문자 = 하나를 붙인다.

r이 2인 경우 패딩문자 = 두개를 붙인다.

Base64 인코딩 순서는 아래와 같다.

1. ASCII 테이블 매핑

2. 2진수 변환

3. 6bit 단위로 분할

4. 10진수 변환

5. Base64 테이블 매핑

6. 패딩 연산

문자 인코딩 방법은 Base64외에도 ASCII, URL, HTML 등이 있으며, allstar927님의 블로그에 '인코딩이란? (ASCII, URL, HTML, Base64, MS Script 인코딩)'이란 제목으로 깔끔하게 정리되어 포스팅이 되어있다.

Base64의 자바구현은 다양한 벤더에서 제공하고 있지만 Touch Me 에서는 Apache Commons Codec을 사용하였다.


내일이면 주문한 도서가 도착할 듯하다. 터치미 서비스의 신뢰성과 안전성을 위해서 체계적으로 정리해서 적용해봐야겠다.




* 참고 글

1. allstar927님 - 인코딩이란? (ASCI, URL, HTML, Base64, MS Script 인코딩)

2. www.java2s.com - Generate a RSA public key with given modulus and public/private exponent

3. www.javamex.com - RSA ecryption in Java

4. 剛宇님 - 자바 암호화 - RSA

5. 류지현(瀏智賢)님 - 자료구조 8. 대칭형 암호화, 비대칭형 암호화

6. 권남님 - RSA 기반 웹페이지 암호화 로그인

7. 오늘도 커피 3잔? - AES를 이용한 128비트 공통키의 생성과 암호화

8. 위키피디아 - RSA 암호 (영문, 한글)

+ Recent posts