Programming/Language Tip

Node.js 와 Android 사이 RSA, AES 암호화 예제

awesometic 2016. 8. 2. 19:15
반응형

프로젝트를 하나 진행하는데.. 소켓 데이터 암호화가 필요한 시점이 왔었다. Node.js 에 npm 모듈로 socket.io 를 올린 서버와, socket.io 라이브러리를 사용하는 안드로이드 어플리케이션. 둘 사이에 JSON 데이터를 socket.io를 통해 주고 받는다. 이 데이터가 암호화되지 않은 평문이라면, 아마 와이어샤크 등으로 쉽게 모든 개인 정보가 보여졌을 것이다.

처음엔 공개 키 암호화 방식으로 RSA 암호화만을 사용하려고 했었다. 아주 강력한 암호화 방식. RSA Key Pair(공개 키, 비밀 키 한 쌍)을 생성하고, 공개 키로 암호화한 건 비밀 키로만 복호화할 수 있는 방식이다. 하지만, RSA 암호화 방식은 적은 양의 데이터(평문 데이터 245byte 이하)만 암호화가 가능하다고 해, JSON 암호화에는 부적절했다. JSON 은 키 하나와 값 하나만 들어가더라도, 문자열로 { "key":"value" } 가 되어 너무 길어진다. 그리고, RSA 암호화는 그 알고리즘이 복잡하여 Key Pair 생성 시간이 상대적으로 너무 오래 걸려 성능 상으로도 부적절했다.

그래서 알아보니, AES 암호화를 같이 곁들여 사용하는 게 일반적이라고 한다. AES 암호화는 대칭 키 알고리즘이다. 하나의 비밀 키를 만들어, 그 비밀 키로 암호화 및 복호화를 하는 방식이다. AES 암호화는 미국 정부에서 DES 를 대체해 사용할 만큼 강력하지만, 알고리즘은 간단하여 성능상으로도 큰 문제가 없다. 무엇보다, 평문 데이터의 길이에 상관이 없이 암호화가 가능하다.

어떻게 RSA와 AES를 적절히 볶아 쓰는가?

간단했다. AES 는 대칭 키 하나를 가지고 암호화와 복호화 둘 다 해버린다. RSA 는 공개 키로 암호화 하고, 비밀 키로 복호화한다. 그렇다면, 서버에서 RSA Key Pair 를 만들고, RSA 공개 키를 클라이언트에 알리고 난 뒤 클라이언트는 AES를 통해 평문 데이터를 암호화하고, RSA 공개 키를 통해 AES 대칭 키를 암호화 해 서버로 보내면 된다. 받은 서버는 RSA 비밀 키로 암호화된 AES 대칭 키를 복호화한 뒤, 그 AES 대칭 키로 AES 암호화 된 평문 데이터를 복호화하면 될 것이다.

조금(?) 복잡해 보이는데, 이렇게들 쓴다더라

그래서 이렇게 적용하도록 했고, 얼추 적용은 시켰다. 근데 이 암호화 관련 예제 소스가 영어로 구글링하면 나오긴 하는데 Node.js와 안드로이드 사이 한 번에 작동하는 코드를 찾기 힘들어 직접 올린다

RSA 2048bit 키와 AES 256bit 키이다.

Node.js 에선 npm 모듈 패키지로 node-rsa 를 추가 설치했다.


RSA encryption example for Node.js using node-rsa

/**
* Created by Awesometic
* references: https://github.com/rzcoder/node-rsa
* RSA encryption example for Node.js with node-rsa. It's encrpyt returns Base64 encoded cipher, also decrypt for Base64 encoded cipher. RSA public key be returned in PKCS8 format.
* License for node-rsa
Copyright (c) 2014 rzcoder<br/>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var NodeRSA = require('node-rsa');
var key = new NodeRSA( { b: 2048 } );
// usage in code: (nodeRsa is a variable of this)
// nodeRsa.encrypt(plainData) or nodeRsa.encrypt(plainData, rsaPublicKey)
var encrypt = function() {
switch (arguments.length) {
case 1:
return key.encrypt(arguments[0], 'base64');
case 2:
tempKey = new NodeRSA();
tempKey.importKey(arguments[1], 'pkcs8-public-pem');
return tempKey.encrypt(arguments[0], 'base64');
default:
return null;
}
};
var decrypt = function(cipherText) {
return key.decrypt(cipherText, 'utf8');
};
var getKeyPair = function() {
return key;
};
var getPublicKey = function() {
return key.exportKey('pkcs8-public-pem');
};
module.exports = NodeRSA;
module.exports.encrypt = encrypt;
module.exports.decrypt = decrypt;
module.exports.getKeyPair = getKeyPair;
module.exports.getPublicKey = getPublicKey;
view raw rsaCipher.js hosted with ❤ by GitHub


AES encryption example for Node.js

/**
* Created by Awesometic
* references: https://gist.github.com/ericchen/3081970
* This source is updated example code of above source code.
* I added it two functions that are make random IV and make random 256 bit key.
* It's encrypt returns Base64 encoded cipher, and also decrpyt for Base64 encoded Cipher
*/
var crypto = require('crypto');
var AESCrypt = {};
AESCrypt.encrypt = function(cryptKey, crpytIv, plainData) {
var encipher = crypto.createCipheriv('aes-256-cbc', cryptKey, crpytIv),
encrypted = encipher.update(plainData, 'utf8', 'binary');
encrypted += encipher.final('binary');
return new Buffer(encrypted, 'binary').toString('base64');
};
AESCrypt.decrypt = function(cryptKey, cryptIv, encrypted) {
encrypted = new Buffer(encrypted, 'base64').toString('binary');
var decipher = crypto.createDecipheriv('aes-256-cbc', cryptKey, cryptIv),
decrypted = decipher.update(encrypted, 'binary', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
};
AESCrypt.makeIv = crypto.randomBytes(16);
// Change this private symmetric key salt
AESCrypt.KEY = crypto.createHash('sha256').update('Awesometic').digest();
module.exports = AESCrypt;
view raw aes256Cipher.js hosted with ❤ by GitHub


RSA encryption example for Android

/**
* Created by Awesometic
* It's encrypt returns Base64 encoded, and also decrypt for Base64 encoded cipher
* references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
*/
import android.util.Base64;
import java.nio.charset.StandardCharsets;
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.X509EncodedKeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
public class RSACipher {
KeyPairGenerator kpg;
KeyPair kp;
PublicKey publicKey;
PrivateKey privateKey;
byte[] encryptedBytes, decryptedBytes;
Cipher cipher, cipher1;
String encrypted, decrypted;
private final static String CRYPTO_METHOD = "RSA";
private final static int CRYPTO_BITS = 2048;
public RSACipher()
throws NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
IllegalBlockSizeException,
BadPaddingException {
generateKeyPair();
}
private void generateKeyPair()
throws NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
IllegalBlockSizeException,
BadPaddingException {
kpg = KeyPairGenerator.getInstance(CRYPTO_METHOD);
kpg.initialize(CRYPTO_BITS);
kp = kpg.genKeyPair();
publicKey = kp.getPublic();
privateKey = kp.getPrivate();
}
/**
* Encrypt plain text to RSA encrypted and Base64 encoded string
*
* @param args
* args[0] should be plain text that will be encrypted
* If args[1] is be, it should be RSA public key to be used as encrypt public key
* @return a encrypted string that Base64 encoded
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws IllegalBlockSizeException
* @throws BadPaddingException
*/
public String encrypt(Object... args)
throws NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
IllegalBlockSizeException,
BadPaddingException {
String plain = (String) args[0];
PublicKey rsaPublicKey;
if (args.length == 1) {
rsaPublicKey = this.publicKey;
} else {
rsaPublicKey = (PublicKey) args[1];
}
cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey);
encryptedBytes = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
return Base64.encodeToString(encryptedBytes, Base64.DEFAULT);
}
public String decrypt(String result)
throws NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
IllegalBlockSizeException,
BadPaddingException {
cipher1 = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
cipher1.init(Cipher.DECRYPT_MODE, privateKey);
decryptedBytes = cipher1.doFinal(Base64.decode(result, Base64.DEFAULT));
decrypted = new String(decryptedBytes);
return decrypted;
}
public String getPublicKey(String option)
throws NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
IllegalBlockSizeException,
BadPaddingException {
switch (option) {
case "pkcs1-pem":
String pkcs1pem = "-----BEGIN RSA PUBLIC KEY-----\n";
pkcs1pem += Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT);
pkcs1pem += "-----END RSA PUBLIC KEY-----";
return pkcs1pem;
case "pkcs8-pem":
String pkcs8pem = "-----BEGIN PUBLIC KEY-----\n";
pkcs8pem += Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT);
pkcs8pem += "-----END PUBLIC KEY-----";
return pkcs8pem;
case "base64":
return Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT);
default:
return null;
}
}
public static PublicKey stringToPublicKey(String publicKeyString)
throws NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
IllegalBlockSizeException,
BadPaddingException {
try {
if (publicKeyString.contains("-----BEGIN PUBLIC KEY-----") || publicKeyString.contains("-----END PUBLIC KEY-----"))
publicKeyString = publicKeyString.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
byte[] keyBytes = Base64.decode(publicKeyString, Base64.DEFAULT);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(spec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
return null;
}
}
}
view raw RSACipher.java hosted with ❤ by GitHub


AES encryption example for Android

/** Created by Awesometic
* references: https://gist.github.com/dealforest/1949873
* This source is updated example code of above source code.
* I added it two functions that are make random IV and make random 256 bit key.
* It's encrypt returns Base64 encoded cipher, and also decrpyt for Base64 encoded Cipher
*/
import android.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidAlgorithmParameterException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
public class AES256Cipher {
public static byte[] getRandomAesCryptKey() {
try {
MessageDigest sha256Hash = MessageDigest.getInstance("SHA-256");
sha256Hash.update(Constants.AES256_KEY_SALT.getBytes(StandardCharsets.UTF_8));
return sha256Hash.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
public static byte[] getRandomAesCryptIv() {
byte[] randomBytes = new byte[16];
new SecureRandom().nextBytes(randomBytes);
return new IvParameterSpec(randomBytes).getIV();
}
public static String encrypt(byte[] aesCryptKey, byte[] aesCryptIv, String plainText)
throws java.io.UnsupportedEncodingException,
NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
InvalidAlgorithmParameterException,
IllegalBlockSizeException,
BadPaddingException {
AlgorithmParameterSpec ivSpec = new IvParameterSpec(aesCryptIv);
SecretKeySpec newKey = new SecretKeySpec(aesCryptKey, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec);
return Base64.encodeToString(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT);
}
public static String decrypt(byte[] aesCryptKey, byte[] aesCryptIv, String cipherText)
throws java.io.UnsupportedEncodingException,
NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidKeyException,
InvalidAlgorithmParameterException,
IllegalBlockSizeException,
BadPaddingException {
AlgorithmParameterSpec ivSpec = new IvParameterSpec(aesCryptIv);
SecretKeySpec newKey = new SecretKeySpec(aesCryptKey, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, newKey, ivSpec);
return new String(cipher.doFinal(Base64.decode(cipherText, Base64.DEFAULT)), "UTF-8");
}
}


공통적으로, 이들 소스 코드 내 모든 encrypt 와 decrypt 함수(메소드)들은 모두 Base64 인코딩을 기준으로 한다.

여담으로... 보통 웹 상 로그인에 적용하지는 않는다. 웹에선 암호화는 SSL 이하 잡 으로 취급하는 분위기. 웹에선 SSL이 최고인 듯

반응형