여자친구가 Umano로 공부를 하고 있는데, Umano 개발팀이 드랍박스로 합류하게 되어 Umano 서비스가 종료되는 바람에 Umano MP3를 다운로드하고, Umano 포스트의 원문 기사의 내용을 텍스트로 추출하는 작업을 진행했다.

mp3 파일로부터 음성인식을 통해 텍스트를 추출하려고 조사를 하다보니 100% 정확하게 음성을 인식할 수도 없고, 라이브러리 차원에서 해결하기도 어려운 문제인지라 원문 기사 링크에서 기사 본문을 추출하는 방향으로 진행하기로 했다. 또한 음성인식을 통해 텍스트를 추출한다면 처리량이 쓸데 없이 많아지기도 했다.


HTML로부터 추출한 기사에는 본문 내용만이 포함되어야 하고(웹 페이지를 구성하는 기타 내용을 제외한) 텍스트로 출력될 수 있어야 했다. 즉 에버노트 크롬 확장인 Clearly와 같은 기능을 원했다.

extract contents from web page를 키워드로 찾아보니 딱 원하는 기능을 제공하는 Boilerpipe라는 자바로 작성된 라이브러리를 찾을 수 있었다.



| 기사 추출

먼저 Boilerpipe의 다운로드 페이지에서 라이브러리 파일을 받은 후 클래스패스에 추가한다. 아쉽게도 메이븐 중앙 저장소에는 없다. 2011년 7월을 끝으로 더 개발이 진행되진 않는 것 같지만 쓰고자 하는 기능상으로는 문제가 없는 듯하다.




사용 방법은 간단하다. 아래와 같이 사용한다.



| User-Agent 헤더로 인한 403 응답

Umano는 여러 뉴스 제공사의 기사를 라디오 뉴스처럼 제공하기 때문에 일부 뉴스에서는 요청 파라미터나 헤더 없이도 기사 내용을 잘 가져오지만, 일부는 403이나 OK이 외의 다른 응답과 함께 페이지 내용을 받을 수 없다.

Bolierpipe에서 HTTP 요청에 대한 커스터마이징을 제공하지 않기 때문에 OkHTTP 라이브러리를 통해 먼저 HTML 페이지의 내용을 문자열로 받은 후 ArticleExtractor의 getText(String) 메서드를 호출하여 다시 본문 내용을 추출하는 쪽으로 했다.

User-Agent는 구글 크롬에서 요청 시 사용하는 User-Agent 값을 그냥 복사하여 넣어주었다.


또한 SSL 인증서를 검증하는 과정에서 유효기간이 지난 SSL 인증서를 사용하거나 Self-signed 인증서 역시 예외를 발생시키므로 이 과정을 무시하도록 하기 위해 OkHttpClient에 SSL Context 설정한다.





※ 참고 자료

https://code.google.com/p/boilerpipe/wiki/JavaDoc

http://stackoverflow.com/questions/2529682/setting-user-agent-of-a-java-urlconnection

http://stackoverflow.com/questions/25509296/trusting-all-certificates-with-okhttp

http://www.snip2code.com/Snippet/25364/Get-OkHttpClient-which-ignores-all-SSL-e




리눅스에서 curl로 JSON을 받았을 때 Pretty Print가 되어있지 않으면 참 읽기 난감하다.

내용을 복사해서 JSON Viewer로 옮겨서 봐도 되지만 매번 그렇게 하기에는 번거로운 일이기도 해서 "cli json pretty print"을 키워드로 구글링하니 편한 방법을 찾을 수 있었다.


파이썬 2.6 이상이 설치되어 있다면 아래와 같이 커맨드를 실행시키면 된다.

$ echo '{"foo": "lorem", "bar": "ipsum"}' | python -m json.tool

{

    "bar": "ipsum", 

    "foo": "lorem"

}


그럼 위의 출력 결과처럼 보기 좋게 나온다.



참고 자료

http://stackoverflow.com/questions/352098/how-can-i-pretty-print-json


개발 환경

- IDE : Spring Tool Suite 3.3.0.RELEASE

- Spring Framework 3.1.1 (Web MVC 포함)

- OS : Max OSX 10.8.5


연습으로 해본 이미지 업로드 프로젝트입니다. 잘못된 내용이 있거나 개선할 내용이 있으면 댓글 남겨주시면 배우도록 하겠습니다 :)



먼저 STS에서 Spring MVC Web 프로젝트를 하나 만듭니다.








프로젝트 탐색기에 보이는 프로젝트의 모습입니다. 위 단계까지 따라오셨으면 아래에 보이는 파일 중에 없는 것 몇가지가 있습니다. 이 글을 끝까지 따라가면 보이는 프로젝트 파일의 모습입니다.

예제 프로젝트에서는 패키지를 따로 나누지 않고 통패키지(..)에 모든 클래스 파일을 담았습니다.






먼저 servlet-context.xml 파일에 Multipart 사용을 위해서 멀티파트 리졸버를 추가합니다.


<!-- Multipart를 받기 위한 리졸버 추가 -->
<beans:bean id="multipartResolver"
	class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>




MultipartResolver를 사용하게 되면 두개의 라이브러리가 추가적으로 필요합니다. Commons File Upload와 IO입니다.

메이븐을 사용한다면 의존 라이브러리를 추가해주고, 그 외 빌드 툴이나, 혹은 사용하지 않는다면 환경에 맞게, 두가지의 라이브러리를 추가해줍니다.

<!-- Apache Commons File Upload -->
<dependency>
	<groupId>commons-fileupload</groupId>
	<artifactId>commons-fileupload</artifactId>
	<version>1.2</version>
</dependency>

<!-- Apache Commons IO 추가 -->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-io</artifactId>
	<version>1.3.2</version>
</dependency><






이미지 업로드 컨트롤러 클래스 내용입니다.

이미지 업로드를 위한 폼 페이지와 결과 페이지 및 이미지를 얻기 위한 URL을 매핑합니다.

package com.nnoco.example;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class ImageUploadController {
	/*
	 * ImageView는 파일 시스템에 있는 이미지 파일을 응답으로 돌려주는 역할을 합니다.
	 * 뒷 부분에서 ImageView 클래스를 작성하게 됩니다.
	 */
	@Resource(name="imageView") ImageView imageView;

	/**
	 * 이미지를 관리하는 서비스 계층 클래스입니다. 예제에서는 디비를 사용하긴 버거워지므로
	 * 서비스 클래스를 따라하는 모양만 서비스인 클래스입니다.
	 */
	@Autowired ImageService imageService;
	
	/**
	 * 이미지 업로드를 위한 페이지 매핑 
	 */
	@RequestMapping("/uploadPage")
	private String uploadView() {
		return "upload";
	}
	
	/**
	 * 이미지 업로드 페이지의 폼에서 전송 시 받게 되는 메서드 
	 */
	@RequestMapping(value="/upload", method=RequestMethod.POST)
	private String upload(@RequestParam MultipartFile imageFile, ModelMap modelMap) {
		ImageFile fileInfo = imageService.save(imageFile);
		
		modelMap.put("imageFile", fileInfo);
		
		return "uploadComplete";
	}
	
	@RequestMapping("/image/{imageId}")
	private ImageView getImage(@PathVariable String imageId, ModelMap modelMap) {
		ImageFile imageFile = imageService.get(imageId);
		
		modelMap.put("imageFile", imageFile);
		
		return imageView;
	}
}




업로드한 이미지 정보를 담고있는 ImageFile 클래스입니다.

package com.nnoco.example;

public class ImageFile {
	/**
	 * 업로드한 이미지 파일이 저장될 경로
	 */
	public static final String IMAGE_DIR = "/web/upload_images/";

	private String id;
	private String contentType;
	private int contentLength;
	private String fileName;

	public ImageFile(String id, String contentType, int contentLength,
			String fileName) {
		this.id = id;
		this.contentType = contentType;
		this.contentLength = contentLength;
		this.fileName = fileName;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getContentType() {
		return contentType;
	}

	public void setContentType(String contentType) {
		this.contentType = contentType;
	}

	public int getContentLength() {
		return contentLength;
	}

	public void setContentLength(int contentLength) {
		this.contentLength = contentLength;
	}

	public String getFileName() {
		return fileName;
	}

	public void setFileName(String fileName) {
		this.fileName = fileName;
	}

}






이미지 뷰 클래스입니다.

package com.nnoco.example;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;


@Component("imageView")
public class ImageView extends AbstractView{
	@Override
	protected void renderMergedOutputModel(Map model,
			HttpServletRequest req, HttpServletResponse res) throws Exception {
		ImageFile imageFile = (ImageFile)model.get("imageFile");
		
		// 응답 메시지에 파일의 길이를 넘겨줍니다.
		res.setContentLength(imageFile.getContentLength());

		// 응답의 타입이 이미지임을 알려줍니다.
		res.setContentType(imageFile.getContentType());
		
		// 파일로부터 byte를 읽어옵니다.
		byte[] bytes = readFile(imageFile.getFileName());
		write(res, bytes);
	}

	/**
	 * 파일로부터 byte 배열 읽어오기 
	 */
	private byte[] readFile(String fileName) throws IOException {
		String path = ImageFile.IMAGE_DIR + fileName;
		
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
		int length = bis.available();
		byte[] bytes = new byte[length];
		bis.read(bytes);
		bis.close();
		
		return bytes;
	}

	/**
	 * 응답 OutputStream에 파일 내용 쓰기
	 */
	private void write(HttpServletResponse res, byte[] bytes) throws IOException {
		OutputStream output = res.getOutputStream();
		output.write(bytes);
		output.flush();
	}
}




이미지 서비스 클래스입니다.

모양만 서비스이고, 구현은 전혀 아닙니다. 맵 자료구조를 사용해서 DB를 대신했습니다.

package com.nnoco.example;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
public class ImageService {
	private Map imageFilesMap;
	
	public ImageService() {
		init();
	}
	
	/**
	 * 초기화
	 */
	private void init() {
		imageFilesMap = new HashMap();
	}
	
	/**
	 * ID로 이미지 파일 가져오기
	 */
	public ImageFile get(String id) {
		return imageFilesMap.get(id);
	}
	
	/**
	 * Multipart File을 파일로 저장하고 DB(를 빙자한 맵)에 업로드 파일 정보 저장, 실패하는 경우 null리
	 */
	public ImageFile save(MultipartFile multipartFile) {
		// UUID로 유일할 것 같은 값 생성.. 낮은 확률로 중복 가능성이 있음
		String genId = UUID.randomUUID().toString();
		ImageFile imageFile = null;
		
		try {
			String savedFileName = saveToFile(multipartFile, genId);
			
			imageFile = new ImageFile(genId, 
					multipartFile.getContentType(),
					(int)multipartFile.getSize(),
					savedFileName);
			
			imageFilesMap.put(genId, imageFile);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return imageFile;
	}
	
	/**
	 * Multipart File의 내용을 파일로 저장, 저장 후 저장된 파일 이름을 반환
	 */
	private String saveToFile(MultipartFile src, String id) throws IOException {
		String fileName = src.getOriginalFilename();
		byte[] bytes = src.getBytes();
		String saveFileName = id + "." + getExtension(fileName);
		String savePath = ImageFile.IMAGE_DIR + saveFileName;

		/* 파일 쓰기 */
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(savePath));
		bos.write(bytes);
		bos.flush();
		bos.close();
		
		return saveFileName;
	}
	
	/**
	 * 파일이름으로부터 확장자를 반환하는 메서드
	 * 파일이름에 확장자 구분을 위한 . 문자가 없거나. 가장 끝에 있는 경우는 빈문자열 ""을 리턴
	 */
	private String getExtension(String fileName) {
		int dotPosition = fileName.lastIndexOf('.');
		
		if (-1 != dotPosition && fileName.length() - 1 > dotPosition) {
			return fileName.substring(dotPosition + 1);
		} else {
			return "";
		}
	}
}


파일 업로드를 위한 JSP 페이지와 결과 확인 페이지입니다.

upload.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>이미지 업로드</title>
</head>
<body>
	<form action="./upload" method="post" enctype="multipart/form-data">
		<input type="file" name="imageFile"><br>
		<input type="submit" value="전송">
	</form>
</body>
</html>


uploadComplete.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>업로드 결과 페이지</title>
<style type="text/css">
	.failed {
		color: red;
		font-style: bold;
		font-size:18pt;
	}
</style>
</head>
<body>
	<c:choose>
		<c:when test="${imageFile != null }">
		파일 업로드 완료
		<ul>
			<li>파일 ID : ${imageFile.id }</li>
			<li>저장된 파일 이름 : ${imageFile.fileName }</li>
			<li>파일 길이 : ${imageFile.contentLength }</li>
			<li>MIME 타입 : ${imageFile.contentType }</li>
		</ul>
		
		<img src="${pageContext.request.contextPath}/image/${imageFile.id}">
		</c:when>
		<c:otherwise>
		<span class="failed">파일 업로드 실패</span>		
		</c:otherwise>
	</c:choose>
</body>
</html>




여기까지 필요한 설정과 파일을 추가하고, 서버에 올려 접속을 하면 아래와 같은 아주 간단한 폼과 함께 파일을 업로드 해볼 수 있습니다.

이미지 업로드라고 하긴 했지만 사실 이미지 업로드를 위해 한 일은 없고, 파일 업로드를 하는 작업만 한 것 같네요.





위 예제 프로젝트는 아래 파일을 다운로드하시어 살펴보실 수 있습니다.

ImageUploadExample.zip


  1. 익명 2014.01.06 10:54

    비밀댓글입니다

    • 마음이 뛰다 2014.01.09 23:13 신고

      ImageFile 클래스를 보시면 상수로 정의된 IMAGE_DIR 문자열이 있습니다.
      IMAGE_DIR이 이미지 파일이 저장되는 경로이고, 저는 Mac OS 환경이라 /web/upload_images/ 라는 경로 형식이고,
      만약 윈도우즈 환경이시면 c:\어디어디\어디어디 이렇게 지정해주시면 됩니다.
      실제로 저장이 되기 위해서는 c:\어디어디\어디어디 라는 경로가 존재해야하니까 폴더를 생성해주시면 됩니다.

    • 마이찬 2017.08.31 11:42

      덕분에 좋은 거 알아갑니다 고마워요!!

  2. 2017.12.12 16:03

    좋은글 보고갑니다. 윈도우 사용자에 이클립스 쓰시는분들은 이클립스를 관리자 권한으로 실행하신 후 하면 이미지 파일이 저장 되더라구요 참고하세요~

지금 개발하고 있는 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