보안/암호학

[암호화] 인코딩, 디코딩, 해시, 암호화, 복호화

멍개. 2022. 8. 28. 15:52

언제부턴가 인코딩, 해시, 암호화를 혼용하여 사용하는 나 자신을 발견하게 되었다...

우선 분류부터 해야한다.

● 인코딩 / 디코딩

인코딩 디코딩은 문자열을 바이트 코드로 바꾸는것을 의미합니다. 대표적으로 base64, url 인코딩이 있습니다. 디코딩은 바이트 코드를 다시 문자열로 복구하는 것을 의미합니다.

인/디코딩 대표적인 방식으로는 base64와 url 인코딩이 있습니다.

인코딩의 목적은 서로 다른 시스템간 동일한 포맷으로 데이터를 주고받기 위해 사용합니다. 인코딩은 암호화와 완전히 다른 목적을 가집니다.

· base64

base64는 64진법을 의미합니다. Base64는 주어진 문자를 아스키 코드로 변환한 후 2진수로 변환합니다. 그리고 6비트씩 잘라서 나온 값을 base64표를 참고하여 변환한 결과가 base64인코딩 결과입니다.

출처: 나무위키

Base64 인코딩 결과를 Text content로 돌아가는 과정을 디코딩이라고 합니다.

console.log(btoa('Man')) // base64 인코딩
// TWFu

console.log(atob('TWFu')) // base64 디코딩
// Man

base64는 단순히 텍스트 뿐 아니라 이미지 데이터를 표현하는 방식으로도 사용합니다.

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAYAAABIdFAMAAAA....

 

· url 인코딩

url 인코딩은 네트워크 상에서 아스키코드만 해석할 수 있는 환경에서 한글 또는 특수문자 같은 것들을 위해 사용합니다.

헬로우 WORLD

여기엔 아스키코드로 해석이 안되는 4개의 캐릭터가 있습니다. '헬', '로', '우' 그리고 마지막으로 공백입니다.

해당 텍스트를 url 인코딩하면 다음과 같은 결과를 얻습니다.

%ED%97%AC%EB%A1%9C%EC%9A%B0+WORLD

자바스크립트에서는 URL 인코딩을 위해 인터페이스를 제공합니다.

▶ encodeURI()

 
const url = 'http://www.naver.com/멍개'
console.log(encodeURI(url))

// http://www.naver.com/%EB%A9%8D%EA%B0%9C

encodeURI()는 uri 표현에 사용하는 특수문자인 /, :, ., +, = 는 인코딩하지 않습니다.

decodeURI('http://www.naver.com/%EB%A9%8D%EA%B0%9C')

// http://www.naver.com/멍개

▶ encodeURIComponent()

const url = 'http://www.naver.com/멍개'
console.log(encodeURIComponent(url))

// http%3A%2F%2Fwww.naver.com%2F%EB%A9%8D%EA%B0%9C

encodeURIComponent는 /, :, +, =와 같이 URI에 표현되는 특수문자도 인코딩합니다.

decodeURIComponent('http%3A%2F%2Fwww.naver.com%2F%EB%A9%8D%EA%B0%9C')

// http://www.naver.com/멍개

 

● 암호화 / 복호화

암호화는 무결성과 기밀성을 유지하기 위해 사용합니다. 암호화는 크게 단방향과 양방향이 존재합니다.

여기서 단방향은 암호화만 되는것을 의미하며 복호화까지 되는것을 양방향이라고 합니다.

· 복호화가 불가능한 단방향 암호화 - 해시

단방향 암호화의 대표적인 방식이 해시입니다. 해시의 특징은 입력 데이터의 크기와 상관없이 항상 같은 크기의 결과를 얻습니다.

단방향 암호화인 해시를 사용하는 대표적인 예가 블록체인과 JWT 유효성 검사입니다.

출처:  https://namu.wiki/w/%ED%95%B4%EC%8B%9C

nodejs에선 crypto를 이용하여 해시 연산이 가능합니다.

const crypto = require('crypto');

const createPW = pw => crypto.createHash('sha512').update(pw).digest('hex')
console.log(createPW('멍개'))

createHash는 단방향(해시) 알고리즘을 전달합니다,.

update는 암호화할 문자열을 전달합니다.(plain text)

digest는 출력 결과를 표시할 형식을 지정합니다.(hex는 16진수 형태로 표시하는 것을 의미합니다.)

7a28c5137e44d896da0f9d3afa2ecd02f9e65a3a54835b4d5605f75362878fa48a23c54c6e60a71b1f355ad33ccbf76dd700582212499ba61c504399e8665b81

이렇게 나온 결과는 복호화가 불가능합니다.

또한 해시는 해시 충돌이 발생할 수 있어 pdkdf2(Password-based key derivation function version 2) 방식인 더 복잡한 방법을 사용합니다.

const salt = 'slat 소금 짜당~';
console.log('salt', salt);
crypto.pbkdf2('plain text멍개멍개멍개', salt, 1203947, 64, 'sha512', (err, key) => {
  console.log('password', key.toString('base64'));
});

// salt slat 소금 짜당~
// password DC7aX5dtmva6TXdjRsdlrtJbE3E1cUzwLgo0OOl0tAwQHFIWgp3Wgzp+zQKEuymItiiQ1w+CilyWrygkHxHSaA==

salt는 plain text를 암호화 할 때 사용하는 키입니다. 해당 키는 절대 외부에 노출하면 안됩니다.

· 복호화가 가능한 양방향 암호화

양방향 암호화는 key를 이용하여 암호화를 합니다. 그리고 key를 이용하여 복호화를 수행합니다.

이때 암호화와 복호화 모두 동일한 키를 사용하는 것을 대칭키(비밀키) 방식이라고 합니다. 암호화와 복호화에 다른 키를 이용하는 것을 비대칭키(공개키) 방식이라고 합니다.

▶ 대칭키(비밀키) 방식

대칭키에 대표적인 알고리즘은 2가지로 나뉩니다.

블록 암호 알고리즘: AES, DES, 3DES, SEED, ARIA

스트림 암호 알고리즘: RC-4

const crypto = require('crypto');

const aesKey = crypto.randomBytes(32) // 32 byte
const iv = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); 

const aes256Encrypt = (text)  => {
  let cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
  let result = cipher.update(text, 'utf8', 'base64');
  result += cipher.final('base64');
  return result
}

const aes256Decrypt = (cryptogram) =>  {   
  const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv);
  let result = decipher.update(cryptogram, 'base64', 'utf8');
  result += decipher.final('utf8');
  return result
}

let plainTextData = "멍개입니다"
let encryptStr = aes256Encrypt(plainTextData)
let decryptStr = aes256Decrypt(encryptStr)

console.log("  encrypt : ", encryptStr)
console.log("  decrypt : ", decryptStr)

//  encrypt :  fXkEV6VmssEaWQddJo+uUA==
//  decrypt :  멍개입니다

▶ 비 대칭키(공개키) 방식

비 대칭키 방식은 공개키 방식이라고도 합니다. 비 대칭키 방식은 암/복호화를 위해 한 쌍의 개인키-공개키를 생성합니다. 이때 개인키로 암호화 된 결과는 공개키로만 복호화가 가능하며, 반대도 성립합니다. 단, 공개키로 암호화된 결과는 공개키로 복호화가 불가능합니다.

비대칭키는 두 가지 경우가 있습니다. 개인키로 암호화 하는 경우, 공개키로 암호화 하는 경우

비대칭키 알고리즘은 크게 두 가지로 나뉩니다.

인수분해: RSA

이산대수: DSA

1. 개인키로 암호화 하는 경우 - 전자서명

A 유저는 네트워크를 통해 데이터를 전송합니다. 이때 해당 데이터가 A가 전송하게 맞는지를 어떻게 증명할까요? 이때 사용하는 방식이 전자서명 입니다.

A 유저는 우선 공개키-개인키를 생성합니다. 그리고 개인키로 전송하고자 하는 데이터를 암호화 합니다. 이때 개인키는 절대 노출하면 안됩니다. 그리고 공개키는 외부에 공개를 하여 목적지로 전송합니다. 목적지는 해당 데이터를 전달 받으면 공개된 A 유저의 공개키를 기반으로 복호화 합니다. 정상적으로 복호화가 되었다면 전달받은 데이터가 A 유저가 보낸것을 증명하는 것이 됩니다.

이때 만약 해커 H가 A 유저의 개인키가 아닌 다른 개인키를 이용하여 A 유저가 보낸것처럼 위장을 해도 목적지에서는 A 유저의 공개키를 기반으로 복호화를 시도하지만 정상적으로 복호화가 되지 않기 때문에 A 유저가 보낸 데이터가 아닌것으로 간주합니다.

대표적인 방법이 공인인증서입니다.

2. 공개키로 암호화 하는 경우 - 인증

공개키로 암호화 하는 경우는 다른 사용자가 개인키와 공개키를 가지고 있는 A 유저에게 데이터를 전송할 때 악의적인 유저가 해당 데이터를 감청하더라도 해석할 수 없게 하는 방법입니다.

다른 유저가 A에게 데이터를 전송할 때 A가 공개한 공개키를 기반으로 암호화 하여 A에게 전송합니다. 이때 해커 H가 해당 데이터를 감청하더라도 A가 가지고 있는 개인키가 없으면 해당 데이터를 복호화하여 볼 수 없습니다.

즉, 해당 방식은 감청이 되더라도 원본 데이터를 볼 수 없습니다.

공개키를 암호화 하여 감청이 되더라도 원본 데이터가 유출되는 것을 방지하기 위해 SSL, TLS에서 사용하는 방법입니다.

 
const crypto = require("crypto");

const RSA_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC41wsMRbC4M4zkWYPI85WwYttEgbLQmIiHD4g6Kjz43h0+3rgt\n8m6IJtolraHpIus+izEx03kpYgJiTLAdruQBDqXTgTyIafZpLaZxH8kYVTomfjJL\nzqTbxuHz1uLqqOaeLzvKiKbCbbCaKWolgCQOCUu+rj/qdftchEVq/LcCcwIDAQAB\nAoGAbIo6fpZd04zR6zV1YYdIGy+xumS+8Cbh5Q2F3UH4U9t6KPT4CmMV7PWDnCR9\nsz1CDpQF61BXEanv5HFL6eJNGCH20z4SctlnMZCw0CJvel4tue4mVmAORctXfcy8\nA9gXpP2D3+tWInSRjZqDZt95ca6N5htlf99a7dcebh7xQkECQQDl/KChwiHw4nBk\nGuDlRp1G7TdP5chBDccdS0655957ZJCROcrjglfJobaGNFsjhoEyWPMl+Ywbt6IZ\n1FV7Z0FbAkEAzb8rVjhuiUurkf9boO2gq1EBMhNULJJHZOB50dpXiD8LvzWcBzvm\nYPppsMYUNS35ItaqXpvQLYswyIA3Seq2yQJAPen7qHBlyL58+UYPI0oWTyDPUjAO\n8AxwfR9n6z5Ts65ICQCg8QyG654gUBLKMk8ketRdaOy8Xj3aYs+5z4XlnwJABlxE\nkLPJ5wCp2yeTw5PVBbbJXKzwSzhycJHn8i7XyeR5Dn4vxqF5a8ISBl75PPOg4gzU\n03vpoZ7N8UTVcLmK0QJBAN4B3D/iBTce6Q5q3Apza8jzMS5ZZjh4Y8/A9K6IiQ54\nkul2tfIVywHVFoxJzGm4JMkHSf7yoVjsjEDpQvKMnIk=\n-----END RSA PRIVATE KEY-----"
const RSA_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC41wsMRbC4M4zkWYPI85WwYttE\ngbLQmIiHD4g6Kjz43h0+3rgt8m6IJtolraHpIus+izEx03kpYgJiTLAdruQBDqXT\ngTyIafZpLaZxH8kYVTomfjJLzqTbxuHz1uLqqOaeLzvKiKbCbbCaKWolgCQOCUu+\nrj/qdftchEVq/LcCcwIDAQAB\n-----END PUBLIC KEY-----"

const privateKey = RSA_PRIVATE_KEY.replace(/\\n/g, '\n');
const publicKey = RSA_PUBLIC_KEY.replace(/\\n/g, '\n');

// public key로 암호화
encodeRSAByPublic = (text) => {
  const buffer = Buffer.from(text);
  const encrypted = crypto.publicEncrypt(publicKey, buffer);
  return encrypted.toString("hex");
}

// private key로 암호화
encodeRSAByPrivate = (text) => {
  const buffer = Buffer.from(text);
  const encrypted = crypto.privateEncrypt(privateKey, buffer);
  return encrypted.toString("hex");
}

// private key로 복호화
decodeRSAByPrivate = (text) => {
  const buffer = Buffer.from(text, "hex");
  const decrypted = crypto.privateDecrypt(privateKey, buffer);
  return decrypted.toString("utf8");
}

// public key로 복호화
decodeRSAByPublic = (text) => {
  const buffer = Buffer.from(text, "hex");
  const decrypted = crypto.publicDecrypt(publicKey, buffer);
  return decrypted.toString("utf8");
}

const text = '멍개멍개';

console.log('encrypt: public, decript: private')
const encrypted0 = encodeRSAByPublic(text); 
console.log(`encrypted: ${encrypted0}`);

const decrypted0 = decodeRSAByPrivate(encrypted0); 
console.log(`decrypted: ${decrypted0}`);

console.log('==========================')

console.log('encrypt: private, decript: public')
const encrypted1 = encodeRSAByPrivate(text); 
console.log(`encrypted: ${encrypted1}`);

const decrypted1 = decodeRSAByPublic(encrypted1); 
console.log(`decrypted: ${decrypted1}`);
encrypt: public, decript: private
encrypted: 56f23d4a961151759052e633f0a8762f0c918cee5e6ff8bd335e5043c9fb2048caf3f514bfeb2afba76bad2c9955b80e63188ceb68bc9e543aad155ee0997d98f7023ba6953db4093864bfe37975eec63080e77a4ba92a0a1cde5e096a8e14f1f43e62c92ec8fbfb2768f7520085f5637b3a40cb4b9b9f7f643fc1cfde150aff
decrypted: 멍개멍개

==========================

encrypt: private, decript: public
encrypted: 0d5784fa8fe822ef8de6ae484b1797abef6a855100672b31cb22e065f95a313ff26cee581a22f3edb0abccd34e9a3e2229ba083c6b4d6a00bcbfdbe7f6fcfb2dc832ef83a5fb6a14d0e0f4137eed872e88866b26ff44f805c75c86ae5d27a8092dffee1baac12cf40dea777206c8a1e43393e078cb3ba477753b314d620cac03
decrypted: 멍개멍개

암호화: 공개키, 복호화: 개인키

암호화: 개인키, 복호화: 공개키

2가지 방식 모두 정상적으로 암/복호화를 진행합니다. 만약 암호화와 복호화를 같은키를 이용한다면?

onsole.log('encrypt: public, decript: public')
const encrypted2 = encodeRSAByPublic(text); 
console.log(`encrypted: ${encrypted2}`);

const decrypted2 = decodeRSAByPublic(encrypted2); 
console.log(`decrypted: ${decrypted2}`);
node:internal/crypto/cipher:79
    return method(data, format, type, passphrase, buffer, padding, oaepHash,
           ^

Error: error:0407008A:rsa routines:RSA_padding_check_PKCS1_type_1:invalid padding
    at Object.publicDecrypt (node:internal/crypto/cipher:79:12)
    at decodeRSAByPublic (/Users/jeongtaepark/Desktop/encrypt/rsa.js:33:28)
    at Object.<anonymous> (/Users/jeongtaepark/Desktop/encrypt/rsa.js:61:20)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47 {
  opensslErrorStack: [
    'error:04067072:rsa routines:rsa_ossl_public_decrypt:padding check failed'
  ],
  library: 'rsa routines',
  function: 'RSA_padding_check_PKCS1_type_1',
  reason: 'invalid padding',
  code: 'ERR_OSSL_RSA_INVALID_PADDING'
}

에러가 발생합니다.

공개키 - 개인키는 openssl을 이용하여 생성할 수 있습니다.

$ openssl genrsa -out private.key 2048

$ openssl rsa -in private.key -out public.key -pubout

비대칭키 알고리즘은 대칭키 알고리즘보다 더 많은 연산을 하기 때문에 대칭키보다 연산 결과가 느립니다.