본문 바로가기
프로그래밍/JavaScript

JS에서 RSA 이용하기(키 페어 생성/암호화) - Web Crypto API

by 페이지다운 2022. 4. 29.
반응형

국내에서는 거의 정보가 없어서 올려본다.

 

종단간 암호화 채팅 서비스를 만들어보려고 프론트엔드에서 RSA 암호화를 할 방법이 없나 알아보았는데 암호화 자체는 가능하지만 RSA 키 페어를 생성할 방법이 마땅치가 않았다. 백엔드에서 RSA 키 페어를 생성해주면 종단간 암호화는 아무런 의미가 없기 때문이다.

 

그래서 방법을 알아보다가 거의 유일하게 발견한 방법이 이 API였다. 브라우저에서 지원하는 API라서 신뢰하고 써도 될 것이다.

 

SubtleCrypto에 대한 자세한 정보는 MDN에 나와 있으므로 참고해도 좋다.

 

이 API가 지원하는 RSA 체계는 다음과 같다.

  • RSA-OAEP (암호화)
  • RSASSA-PKCS1-v1_5 (전자서명)
  • RSA-PSS (전자서명)

1. Key Pair 생성

이 API를 이용하는 이유기도 하다. 다음 코드를 참고하면 된다.

const pemHF = {
    public: {
        header: '-----BEGIN PUBLIC KEY-----',
        footer: '-----END PUBLIC KEY-----',
    },
    private: {
        header: '-----BEGIN PRIVATE KEY-----',
        footer: '-----END PRIVATE KEY-----',
    },
};

const arrayBufferToStr = (buf) => {
    return String.fromCharCode.apply(null, new Uint8Array(buf));
};

const exportKey = async (keyType, key) => {
    const exported = await window.crypto.subtle.exportKey(keyType === 'public' ? 'spki' : 'pkcs8', key);
    const exportedAsString = arrayBufferToStr(exported);
    const exportedAsBase64 = window.btoa(exportedAsString);
    const pemExported = pemHF[keyType].header + '\n' + exportedAsBase64 + '\n' + pemHF[keyType].footer;
    return pemExported;
};

const createKeyPair = async () => {
    const keyPair = await window.crypto.subtle.generateKey(
        {
            name: 'RSA-OAEP',
            modulusLength: 2048,
            publicExponent: new Uint8Array([1, 0, 1]),
            hash: 'SHA-256',
        },
        true,
        ['encrypt', 'decrypt']
    );
    const publicKey = await exportKey('public', keyPair.publicKey);
    const privateKey = await exportKey('private', keyPair.privateKey);
    return { publickey: publicKey, privateKey: privateKey };
};

키 페어를 생성하는 함수는 window.crypto.subtle.generateKey 함수이다. 이 함수가 받는 파라미터는 algorithm, extractable, keyUsages 세개인데 우리는 RSA를 이용하므로 맨 처음 넘기는 객체는 RsaHashedKeyGenParams이다.

 

RsaHashedKeyGenParams 객체의 키와 값은 다음과 같다.

  • name: RSA 체계 종류. 위에서 언급했던 세가지 중 하나를 넣으면 된다.
  • modulusLength: 키 길이. 최소 2048이어야 한다. 요즘엔 4096을 권장하며, 심한 경우에는 벌써 8192를 사용하는 경우도 있다.
  • publicExponent: 지수. 65537이 대중적이므로 new Uint8Array([1, 0, 1])를 상수로 넣고 있으면 된다. 
  • hash: diget 함수. SHA-256, SHA-384, SHA-512 중 선택 가능.

extractable은 window.crypto.subtle.exportKey 함수를 통해 키를 추출할 것인지의 여부인데 우리는 당연히 true이다.

 

keyUsages는 키의 용도. 우리는 encrypt와 decrypt 둘 다 사용할 것이다. 만약 서명을 할 생각이라면 encrypt와 decrypt가 아닌 sign과 verify를 넣어야 한다.

 

exportKey 함수에서의 window.crypto.subtle.exportKey는 키를 추출하는 용도이다. 공개키의 포맷은 spki (Subject Public Key Info), 비밀키의 포맷은 pkcs8 (PKCS #8) 라는 것만 주의. 여기서 우리는 ArrayBuffer로 된 키를 문자열(유니코드)로 바꾸고 이를 base64로 변환하게 된다. 그 후에는 pem 형식에 맞춰 헤더와 푸터를 붙인다.

 

이 함수를 실행하면 리턴값은 다음과 같다.

{
    "publickey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApkdPiGPxdfylFbNWowq/YMc3Z5jUDKLuqHcmFU0KbaIGvO9O+BRRDTbRJuBYXgpa7ga9SE7SLChJDu5k4kI+aP2aNu2xs7Mc+vMzHfuzBtIhIh3/8EFI2dQbuOx9OmcEa23+WdritWWed455lmslFb2AktrPqWMgpuYohbPwiksLp8oBWmJvUav+9ungsD9XyhzuC63kFx85CFjtQYBZ7H19qUdy0ai62P9erI3TUOCO0Q/47npQnpZD9nUZxoa/otO9UPeSYcjFGJFBVxJkWzc2IXRq6QvFdbz2KCscFUi6t6gKTkaCtCovHHYyQOPmMZkOKRHXS9kPJVYQ0MH2NwIDAQAB\n-----END PUBLIC KEY-----",
    "privateKey": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCmR0+IY/F1/KUVs1ajCr9gxzdnmNQMou6odyYVTQptoga87074FFENNtEm4FheClruBr1ITtIsKEkO7mTiQj5o/Zo27bGzsxz68zMd+7MG0iEiHf/wQUjZ1Bu47H06ZwRrbf5Z2uK1ZZ53jnmWayUVvYCS2s+pYyCm5iiFs/CKSwunygFaYm9Rq/726eCwP1fKHO4LreQXHzkIWO1BgFnsfX2pR3LRqLrY/16sjdNQ4I7RD/juelCelkP2dRnGhr+i071Q95JhyMUYkUFXEmRbNzYhdGrpC8V1vPYoKxwVSLq3qApORoK0Ki8cdjJA4+YxmQ4pEddL2Q8lVhDQwfY3AgMBAAECggEAA4vknvfkVlJ9W7LuHeu3wzx2HdtWz0iWWh9+2Qt1aOGaaGM0rJPgwROY1TRwJ6lSFF7IByFiccpC+XLAnuOWaZTaU8hkEH1S5YNUZS2EijIaWAKKl+w8iopvzbtPItsmby6MZnKfTT30NrNiPBp5UwP7GKbA5E/J8LInmrQIy414/7C8pMt5r/cx4zNbqjVfKpON4Zzcd5AA0L+A7qNE+Rp87SrFOQm9NIzsNKRBZC/VbtWC8EjABP3oWkMhyK0sfJeMsKjk88O8VV9DFVKhWzgF1ss9Z8c/w1cDbaQwijN1sy7D2m1CwyRvMyeGIrjH1qkDQy3TUbq+HWL0XQHCYQKBgQDXOza0YUWr1qJRAqX2tU007DnSqRfbeqrLLNU1Sahq6KIOW2yU4RL5QUI4z5qtxAj6erT+tAimP0ZPd97sUd2xAaLUubbpLybVbf6bwiEdSFrs4FJT3yU2Hbe0c2nZ1MJ0PTdKYbNBByMe4hJEE5iac9wy5UgI1nJ9BU4dLz9mkQKBgQDFxlQEkfF9XX/x3XlFOqxvk80ed84GVfe/gy4jfCWoi71R/agHG/XvnnSFScNpvt9qF3k87jPtOxl/eGrfp3j4VDC126RKIn/tMUIpqzEuRFA+MH05Ga2AsbqvlUAugr/yjbIEF5fpEd4ozuXyYYF6N0NosNp0ZWtJr5qfkw5ERwKBgQCaQvqGdmF1NDTKU1eYZ4GauOUIs+FBkb3wOYXm15A6b9oW6Df+zQLIW5mXFlUKzkKRJHmtRGQeo6NFOekw/8whHccTKLiMkRsIRfoCsTUCw+VHedAIBRuqmcodL8tiMTfeEDIcwcG0jeUCMY5d9J2ftgLRB7yFeQ49xRujl0WdwQKBgQCXwo0cCKVG7qVMAgeZgOFqqP6f34y/Pd5jHZUc85muELSQotT5x5Hbcjq1QJnrneSv8x08DQjZhB0XvF4+CrN/0pKiKc2SeZCygLblZJDTkOYGIo0kcLi7ZSX8r8lVJ02FIQe1rikKVoSjyceXSwzgrGFUh0jKQjymiVJcGqq65wKBgDPWuhUOncC5P4gHsqDeQ9XnRD3uqdF+gu1liYzoUo/cJitubs6RyDxPuIv2sRnvQca92JsupW6GCIVJgQxH869SbzamCOB/7oLfY/1T3xZ8y1gi4I5+f8R34adKNgGFLkkkZfFbNupzP8fDRq4emAC8cFTfVPNZWFE3PYX5a7zW\n-----END PRIVATE KEY-----"
}

실제 키와 같이 줄바꿈도 되어있는 것을 확인할 수 있다.

2. 암호화 / 복호화

이 키들을 이용해서 암호화와 복호화를 해보도록 하자.

const pemHF = {
    public: {
        header: '-----BEGIN PUBLIC KEY-----',
        footer: '-----END PUBLIC KEY-----',
    },
    private: {
        header: '-----BEGIN PRIVATE KEY-----',
        footer: '-----END PRIVATE KEY-----',
    },
};

const arrayBufferToStr = (buf) => {
    return String.fromCharCode.apply(null, new Uint8Array(buf));
};

const strToArrayBuffer = (str) => {
    const encoder = new TextEncoder();
    return encoder.encode(str).buffer;
};

const encryptRSA = async (text, publicKeyb64) => {
    const binaryDerString = window.atob(publicKeyb64.replace(pemHF.public.footer, '').replace(pemHF.public.header, ''));
    const binaryDer = strToArrayBuffer(binaryDerString);
    const publicKey = await window.crypto.subtle.importKey(
        'spki',
        binaryDer,
        {
            name: 'RSA-OAEP',
            hash: 'SHA-256',
        },
        true,
        ['encrypt']
    );
    const cipher = await window.crypto.subtle.encrypt(
        {
            name: 'RSA-OAEP',
        },
        publicKey,
        strToArrayBuffer(text)
    );
    return window.btoa(arrayBufferToStr(cipher));
};

const decryptRSA = async (cipher, privateKeyb64) => {
    const binaryDerString = window.atob(privateKeyb64.replace(pemHF.private.footer, '').replace(pemHF.private.header, ''));
    const binaryDer = strToArrayBuffer(binaryDerString);
    const privateKey = await window.crypto.subtle.importKey(
        'pkcs8',
        binaryDer,
        {
            name: 'RSA-OAEP',
            hash: 'SHA-256',
        },
        true,
        ['decrypt']
    );
    const text = await window.crypto.subtle.decrypt(
        {
            name: 'RSA-OAEP',
        },
        privateKey,
        strToArrayBuffer(window.atob(cipher))
    );
    return arrayBufferToStr(text);
};

코드가 복잡해 보이지만 실제로 encrypt와 decrypt는 그냥 서로 역순이라고 보면 된다. 이 코드는 공개키를 이용해 암호화하고, 비밀키를 이용해 암호화한다. 반대도 가능하나 본문에는 적지 않는다.

 

아무튼 일단 key를 먼저 import하게 되는데, 그 이전에 pem 형식의 키를 ArrayBuffer로 변환하는 과정이 추가된다.

 

window.subtle.crypto.importKey가 요구하는 파라미터는 format, keyData, algorithm, extractable, keyUsages이다.

 

format은 말 그대로 키의 포맷. 앞서 말했듯이 공개 키는 spki, 비밀 키는 pkcs8이다. 여기서 SPKI는 Simple Public-Key Infrastructure이 아닌 RFC 5280 섹션 4.1에 정의된 Subject Public Key Info를 의미한다. PKCS #8은 RFC 5208에 정의된 그거 맞다.

 

keyData는 키를 말한다. 앞서 ArrayBuffer로 변경한 키를 넣어준다.

 

algorithm은 알고리즘에 대한 정보이다. 객체는 RsaHashedImportParams.

  • name: 암호화 체계. 본 API에서 암호화를 지원하는 RSA 체계는 RSA-OAEP가 유일하다.
  • hash: diget 함수. SHA-256, SHA-384, SHA-512 중 선택 가능.

extractable은 앞서 키를 생성했을 때처럼 API에서 사용하기 위한 CryptoKey 객체로 추출하면서 이를 다시 추출할지를 물어보는 것이다. 

 

keyUsages는 말 그대로 키를 어디에 쓸 것인지 묻는 것이다. 암호화면 encrypt, 복호화면 decrypt. 둘 다 할거면 둘 다 넣으면 된다.

 

그 다음에는 암호화다.

 

window.subtle.crypto.encrypt가 요구하는 파라미터는 algorithm, key, data이다.

 

algorithm은 알고리즘에 대한 정보다. 객체는 RsaOaepParams. 키는 name 하나밖에 받지 않는다. 옵션으로 label이라는 키가 있으나 일반적으로는 필요없다. 궁금하면 MDN 참고 바람.

 

key는 말 그대로 키. 앞서 추출한 CryptoKey 객체를 넣으면 된다.

 

data는 암호화할 데이터. 텍스트를 ArrayBuffer로 바꿔준 뒤 넣으면 된다.

 

그 이후에는 결과물로 나온 ArrayBuffer을 유니코드 문자열로 변경한 후 base64로 변환하게 된다.

 

 

이번에 이렇게 Web Crypto API에서 RSA 키 페어를 생성하고 암호화하는 법을 알아보았다. 전자서명도 있고, 해시도 있고, AES를 이용한 대칭 암호화도 있는데 일단 이번에는 여기까지만. 

 

질문 있으면 댓글 달아주시기 바랍니다.

반응형

댓글