티스토리 뷰

1. 버퍼(buffer)

2. 인코딩(encoding)과 디코딩(decoding)

3. 해시 함수(Hash Function)

4. crypto 모듈을 이용한 암호화

5. 솔트(salt)

6. 토큰 기반 웹 인증 방식

7. JWT(JSON Web Token)


1. 버퍼(buffer)

[그림 1] buffer 설명

버퍼(buffer)는 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역이다. 버퍼링(buffering)이란 버퍼를 활용하는 방식 또는 버퍼를 채우는 동작을 말한다. 다른 말로 '큐(Queue)'라고도 한다.

 

버퍼는 컴퓨터 안의 프로세스 사이에서 데이터를 이동시킬 때 사용된다. 보통 데이터는 키보드와 같은 입력 장치로부터 받거나 프린터와 같은 출력 장치로 내보낼 때 버퍼 안에 저장된다. 버퍼는 하드웨어나 소프트웨어에 추가될 수 있지만 상당수가 소프트웨어에 추가된다. 버퍼는 속도가 계속 바뀔 수 있으므로 데이터 수신, 처리 속도에 차이가 있다.

 

버퍼는 네트워크 상에서 자료를 주고 받을 때나 스피커에 소리를 재생할 때, 또는 디스크 드라이브와 같은 하드웨어의 입출력을 결합하는 데에 자주 이용된다. 버퍼는 순서대로 데이터를 출력하는 FIFO 방식에서 보통 사용된다.

 

 

2. 인코딩(encoding)과 디코딩(decoding)

[그림 2] encoding 및 decoding 과정

흔히 우리가 말하는 인코딩(encoding)은 문자 인코딩(character encoding) 또는 텍스트 인코딩(text encoding)을 의미한다.

 

인코딩은 사용자가 입력한 문자나 기호들을 컴퓨터가 이용할 수 있는 신호로 만드는 것을 말한다. 반대로 컴퓨터가 이용하는 신호를 사용자가 이해할 수 있는 문자나 기호로 바꾸는 과정을 디코딩이라고 한다.

 

넓은 의미의 컴퓨터는 이러한 신호를 입력받고 처리하는 기계를 뜻하며, 신호 처리 시스템을 통해 이렇게 처리된 정보를 사용자가 이해할 수 있게 된다. 이 신호를 입력하는 인코딩과 문자를 해독하는 디코딩을 하기 위해서는 미리 정해진 기준을 바탕으로 입력과 해독이 처리되어야 하는데, 이를 문자열 세트 또는 문자셋이라고 한다.

 

[그림 3] ASCII Table

초기 보급형 컴퓨터의 문자열 세트는 ASCII나 EBCDIC이 표준이었으나, 세계 곳곳에 인터넷이 보급되며 표현해야 할 문자가 증가하면서 이를 포함할 수 있는 대체 방식이 개발되었다. 표준 문자셋을 개발하는 것에 대한 논의가 이어졌고, 후에는 유니코드가 등장하게 되었다.

 

※ 인코딩(encoding) vs. 암호화(encryption)

인코딩과 암호화의 개념을 혼동하지 않아야 한다. 인코딩은 문자열을 '컴퓨터가 이해할 수 있는 신호'로 바꾸는 과정이고, 암호화는 원래 문자열을 허가된 사람들만 사용할 수 있도록 하기 위해 대칭키 및 공개키 알고리즘, 해시함수 등의 수학적 알고리즘을 통해 보안성을 강화하는 과정이다.

 

 

3. 해시 함수(Hash Function)

[그림 4] Hash 개념

해시 함수는 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수이다. 해시 함수에 의해 얻어지는 값은 해시 값, 해시 코드, 해시 체크섬 또는 간단하게 해시라고 한다.

 

해시는 해시 테이블이라는 자료구조에 사용되며, 매우 빠른 데이터 검색을 위한 컴퓨터 소프트웨어에 널리 사용된다. 해시 함수는 큰 파일에서 중복되는 레코드를 찾을 수 있기 때문에 데이터베이스 검색이나 테이블 검색의 속도를 가속할 수 있다. 예로, DNA sequence에서 유사한 패턴을 찾는데 사용될 수도 있다.

 

암호용 해시 함수는 매핑된 해싱 값만 알고 있는 상태에서 원래 입력 값을 알아내기 힘들다는 사실에 의해 사용될 수 있다. 또한, 전송된 데이터의 무결성을 확인하는데 사용되기도 하는데, 메시지가 누구에게서 온 것인지 입증해주는 HMAC를 구성하는 블록으로 사용된다.

 

해시 함수는 결정론적으로 작동해야 하며, 따라서 두 해시 값이 다르다면 그 해시값에 대한 원래 데이터도 달라야 한다. 그 역은 성립하지 않는다. 해시 함수의 질은 입력 영역에서의 해시 충돌 확률로 결정되는데, 해시 충돌 확률이 높을수록 서로 다른 데이터를 구별하기 어려워지고 검색하는 비용이 증가하게 된다. 

 

해시 함수 중에는 암호학적 해시 함수(Cryptographic Hash Function)와 비암호학적 해시 함수로 구분된다. 암호학적 해시 함수의 종류로는 MD5, SHA계열 해시 함수가 있으며, 비암호학적 해시 함수로는 CRC32등이 있다.

 

암호학적 해시 함수는 역상(pre-image), 제2역상(2nd pre-image), 충돌쌍(collision)에 대하여 안전성을 가져야 하며 인증에 이용된다. 암호학적 해시 함수는 임의의 길이를 입력 받지만 MD Strength Padding할 때 길이정보가 입력되므로 최대 길이에 대한 제한이 있다. 예를 들어 패딩시 하위 8비트에 길이정보가 입력되는 경우에는 해시가능한 최대 길이는 0xFF가 되어 255바이트가 된다.

 

 

4. crypto 모듈을 이용한 암호화

암호화 방식은 크게 양방향 암호화와 단방향 암호화로 나뉜다. 양방향 암호화는 암호화와 복호화가 모두 가능한 암호화 방식이고, 단방향 암호화는 암호화는 가능하지만 복호화는 불가능한 암호화 방식이다.

 

Q. 암호화를 했는데 복호화가 불가능하다면 단방향 암호화를 쓸 이유가 있을까?

복호화가 필요하지 않은 경우들이 있다. 예를 들어 홈페이지 비밀번호 같은 경우 비밀번호를 암호화해서 DB에 저장해둔 후, 나중에 로그인할 때 다시 입력받은 비밀번호를 같은 알고리즘으로 암호화해서 DB에 저장된 문자열과 비교하면 된다. 즉, 원래 비밀번호는 어디에도 저장되지 않고 암호화된 문자열로만 비교하게 되는 것이다. 

 

단방향 암호화의 가장 간단한 방식은 해시 함수를 이용하는 것이다.

const crypto = require('crypto');

const sha256 = crypto.createHash('sha256').update('jenny').digest('base64');
const sha256_2 = crypto.createHash('sha256').update('jenny').digest('base16');
const sha256_3 = crypto.createHash('sha256').update('jenny').digest('hex');

console.log(sha256); // I5En4JFXy6+2ISEjsQKqEQMkGUazaEwjLES4Nnw6TUc=
console.log(sha256_2); // <Buffer 23 91 27 e0 91 57 cb af b6 21 21 23 b1 02 aa 11 03 24 19 46 b3 68 4c 23 2c 44 b8 36 7c 3a 4d 47>
console.log(sha256_3); // 239127e09157cbafb6212123b102aa1103241946b3684c232c44b8367c3a4d47

require로 crypto 모듈을 불러와서 createHash 메소드를 사용하면 된다. 인자로 사용할 알고리즘을 넣어주는데, 위의 예시에서는 SHA256으로 했다.

 

update 메소드에는 암호화할 비밀번호를 넣어주고, digest에는 어떤 인코딩 방식으로 암호화된 문자열을 표시할지 정해준다. 인코딩 방식은 base64, hex, latin1 등의 방식이 있는데 base64가 짧아서 더 선호된다.

 

 

5. 솔트(salt)

crypto 모듈을 이용한 암호화 과정에서 같은 알고리즘과 같은 인코딩 방식을 사용하면 하나의 비밀번호에 대해 하나의 결과만을 반환한다. 그래서 비밀번호 암호화에 사용할 수 있다.

 

하지만, 해커가 모든 암호에 대해 어떤 결과가 나올지 데이터베이스화 해두었다면, 결과만 보고도 원래 암호를 유추해낼 수 있을 것이다. 이러한 데이터베이스를 '레인보우 테이블'이라고 한다.

 

따라서 해커가 레인보우 테이블을 사용하지 못하도록 salt를 이용하는 방법이 있다.

 

[그림 5] salt 개념

암호학에서 솔트(salt)는 데이터, 비밀번호, 통과암호를 해시 처리하는 단방향 함수의 추가 입력으로 사용되는 랜덤 데이터이다. 솔트는 스토리지에서 비밀번호를 보호하기 위해 사용된다.

 

const crypto = require('crypto');

const sha256 = crypto.createHash('sha256').update('jenny').digest('base64');
const sha256_2 = crypto.createHmac('sha256', Buffer.from('leejy98')).update('jenny').digest('base64');

console.log(sha256); // I5En4JFXy6+2ISEjsQKqEQMkGUazaEwjLES4Nnw6TUc=
console.log(sha256_2); // gS5cPTewOMZZ1/T9OQmwSeRViuQHT+9pWPvwt8vwXQ4=

위의 예시에서 sha256은 salt를 사용하지 않았을 때의 결과값이고, sha256_2는 salt를 사용했을 때의 결과값이다. salt 값은 'leejy98'을 넣어주었다. 같은 비밀번호를 암호화하더라도 salt값에 따라 결과가 아예 바뀌어 버린다는 것을 알 수 있다.

 

salt 값을 랜덤으로 부여하는 방법도 있다.

crypto.randomBytes(64, (err, buf) => {
    crypto.pbkdf2('jenny', buf.toString('base64'), 100000, 64, 'sha512', (err, key) => {
        console.log(key.toString('base64'));
    });
});

위의 방식을 사용하면 salt값이 코드를 실행할 때 마다 매번 바뀌기 때문에 console.log의 결과값이 매번 다르게 나타난다.

 

randomBytes 메소드로 64비트 길이의 salt를 생성해주었다. buf는 버퍼 형식이므로 buf.toString('base64')로 salt를 base64 문자열로 변경해준다.

 

pbkdf2라는 메소드는 단방향 암호화에서 가장 선호되는 방식 중 하나이다. pbkdf2에는 5개의 인자가 들어가는데 이는 각각 비밀번호, salt, 반복 횟수, 비밀번호 길이, 해시 알고리즘 순이다.  key가 버퍼 형태로 리턴해주기 때문에 base64 방식의 문자열로 만들어서 저장해야 한다.

 

반복 횟수는 해시 함수를 몇 번 반복할 것인지를 나타내고, 위 예시에서는 10만 번으로 해주었다. 이 숫자가 높을수록 슈퍼컴퓨터를 써도 레인보우 테이블을 만들기가 어려워진다. 그리고 숫자도 10만처럼 딱 떨어지는 숫자 외에 104294와 같은 불규칙한 숫자를 사용하는 것이 좋다. 비밀번호 길이는 적당한 길이로 지정해준다.

 

salt값은 매번 바뀌기 때문에 salt값과 비밀번호 값을 항상 같이 저장해두어야 한다.

 

 

6. 토큰 기반 웹 인증 방식

웹 인증(Authentication)은 HTTP를 통해 서버와 클라이언트가 데이터를 주고받을 때 사용한다. HTTP의 경우 서버에게 '누가' 요청했는지 모르기 때문에 클라이언트가 누구인지 확인해야 한다. 이때 사용하는 것이 '인증'이다.

 

사용자 인증 방식은 상태를 저장하느냐, 저장하지 않느냐로 나뉘는데 저장할 경우 세션, 저장하지 않을 경우 토큰을 사용한다.

 

토큰은 인증받은 사용자들에게 토큰을 발급하고, 서버에 요청을 보낼 때 header에 토큰을 함께 보내는 인증 방식이다.

 

사용자가 로그인하면, 서버에서 계정 정보를 검증한 후 토큰을 발급한다. 사용자는 토큰을 저장한 후, 서버에 요청할 때 HTTP Header에 해당 토큰을 함께 전송한다. 그 후 서버에서 토큰을 검증하고, 요청에 응답한다.

 

토큰 인증 방식의 특징

  • 상태를 유지하지 않기 때문에 서버 확장이나 유지, 보수에 용이하다.
  • 토큰을 기반으로 하는 다른 인증 시스템에도 접근이 가능하기 때문에 확장성이 뛰어나다.
  • 쿠키 사용에 대한 취약점이 사라진다.
  • 이미 발급된 토큰은 돌이킬 수 없다. (토큰이 탈취될 경우 유효기간 전까지 정보 탈취가 가능하므로 유효기간을 짧게 설정하는 것이 좋다. Refresh Token을 새로 발급하는 방법도 있다.)

 

 

7. JWT(JSON Web Token)

[그림 6] JWT 구성 요소

JWT는 선택적 서명 및 선택적 암호화를 사용하여 데이터를 만들기 위한 인터넷 표준으로, 페이로드는 몇몇 클레임(claim) 표명(assert)을 처리하는 JSON을 보관하고 있다. JWT는 토큰 기반 방식 중 가장 많이 사용된다.

 

JWT는 Header, Payload, Signature의 3부분으로 이루어져 있으며, 각 부분은 '.'으로 구분된다.

 

JWT를 이용하면 클라이언트는 자신의 정보를 보는 것은 가능하지만 수정은 불가능하다. 그 데이터를 수정하려면 반드시 서버를 통해서만 가능하다.

 

  • Header : 암호화 방식, 타입 등 어떻게 인증할지에 대한 정보를 담고 있다.
  • Payload : 유저의 ID 값, 유효기간 등 서버에서 보내는 데이터가 들어간다.
  • Signature : '.'을 구분자로 해서 헤더와 페이로드를 합친 문자열을 서명한 값을 말한다. 서명은 알고리즘과 비밀키를 이용해 생성하고, base64 URL-safe로 인코딩한다. 따라서 비밀키를 알지 못하면 복호화할 수 없다.

JWT 인증 방식은 다음과 같은 과정을 거친다.

1. 사용자가 로그인하면, 서버에서 확인한 후 고유 ID 값을 부여해 기타 정보와 함께 payload에 넣는다.

2. JWT 토큰 유효기간을 설정한 후, 암호화할 비밀키를 이용해 Access Token을 발급한다.

3. 사용자는 발급된 토큰을 받아 저장한 후, 인증이 필요할 때마다 토큰을 header에 실어 보낸다.

4. 서버에서는 해당 토큰을 복호화 한 후, 조작 여부와 유효기간을 확인해 사용자에게 맞는 데이터를 전달해 준다.

 

const crypto = require('crypto');

const header = {
    alg:'sha256',
    typ:'JWT'
};

const payload = {
    userid:'leejy98',
    name:'jenny'
};

const encodingHeader = Buffer.from(JSON.stringify(header)).toString('base64').replace(/[=]/g, '');
const decodingHeader = JSON.parse(Buffer.from(encodingHeader, 'base64').toString());
const encodingPayload = Buffer.from(JSON.stringify(payload)).toString('base64').replace(/[=]/g, '');
const signature = crypto.createHmac('sha256', Buffer.from('jenny'))
                        .update(`${encodingHeader}, ${encodingPayload}`)
                        .digest('base64')
                        .replace(/[=]/g, '');
const jwt = `${encodingHeader}.${encodingPayload}.${signature}`;
const cookie = {
    token:jwt
};
const [head, pay, sign] = cookie.token.split('.');
const designature = crypto.createHmac('sha256', Buffer.from('jenny'))
                        .update(`${head}, ${pay}`)
                        .digest('base64')
                        .replace(/[=]/g, '');

console.log('header : ', header); // header :  { alg: 'sha256', tpy: 'JWT' }
console.log('encodingHeader : ', encodingHeader); // encodingHeader :  eyJhbGciOiJzaGEyNTYiLCJ0cHkiOiJKV1QifQ
console.log('decodingHeader : ', decodingHeader); // decodingHeader :  { alg: 'sha256', tpy: 'JWT' }
console.log('signature : ', signature); // signature :  IZfF+dDC55HJyS/i09RshDXfykWkg7fPTPiDbW8/rZI
console.log('JWT : ', jwt); // JWT :  eyJhbGciOiJzaGEyNTYiLCJ0cHkiOiJKV1QifQ.eyJ1c2VyaWQiOiJsZWVqeTk4IiwibmFtZSI6Implbm55In0.IZfF+dDC55HJyS/i09RshDXfykWkg7fPTPiDbW8/rZI
console.log('cookie : ', cookie.token); // cookie :  eyJhbGciOiJzaGEyNTYiLCJ0cHkiOiJKV1QifQ.eyJ1c2VyaWQiOiJsZWVqeTk4IiwibmFtZSI6Implbm55In0.IZfF+dDC55HJyS/i09RshDXfykWkg7fPTPiDbW8/rZI
console.log('designature : ', designature); // designature :  IZfF+dDC55HJyS/i09RshDXfykWkg7fPTPiDbW8/rZI

Header는 토큰의 타입과 해시 암호화 알고리즘으로 구성되어 있다.

첫 번째 'alg'는 algorithm의 약자로 HMAC, SHA256, RSA와 같은 해시 알고리즘을 나타내는 부분이다.

두 번째 'typ'는 type의 약자로 토큰 유형을 나타내는 부분이다.

 

Payload에는 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있다. Claim은 3가지로 나누어지며, JSON(Key/Value) 형태로 정보를 담을 수 있다.

 

(1) 등록된 클레임(Registered Claims)등록된 클레임은 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터들로, 모두 선택적으로 작성이 가능하다. 또한, JWT를 간결하게 하기 위해 key는 모두 길이 3의 String을 쓴다. 여기서 subject로는 unique한 값을 사용하는데, 사용자 이메일을 주로 쓴다.

  • iss : 토큰 발급자(issuer)
  • sub : 토큰 제목(subject)
  • aud : 토큰 대상자(audience)
  • exp : 토큰 만료 시간(expiration), NumericDate 형식으로 되어 있어야 함 ex) 1930294
  • nbf : 토큰 활성 날짜(not before), 이 날이 지나기 전의 토큰은 활성화되지 않음
  • iat : 토큰 발급 시간(issued at), 토큰 발급 이후의 경과 시간을 알 수 있음
  • jti : JWT 토큰 식별자(JWT ID), 중복 방지를 위해 사용하며 일회용 토큰(Access Token) 등에 사용

 

(2) 공개 클레임(Public Claims)공개 클레임은 사용자 정의 클레임으로, 공개용 정보를 위해 사용된다. 충돌 방지를 위해 URI 포맷을 이용하며, 예시는 아래와 같다.

{ "http://www.abc.com/admin": true }

 

(3) 비공개 클레임(Private Claims)비공개 클레임은 사용자 정의 클레임으로, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장한다. 아래의 예시가 있다.

{ "token_type": access }

 

Signature는 Token을 인코딩하거나 유효성을 검증할 때 사용하는 고유한 암호화 코드이다. Signature는 위에서 만든 Header와 Payload의 값을 각각 base 64로 인코딩하고, 그 값을 Secret Key를 이용해 Header에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 base 64로 인코딩하여 생성한다.

 

Signature는 Token의 유효성을 검증할 때 사용하는데, 가령 Payload의 name 값을 사용자가 임의로 변경하여 서버에 요청했을 경우 서버는 클라이언트로부터 받은 Signature 값을 해당 사용자의 기존 Signature 값과 비교한다. 그리고 그 값이 다르면 조작된 데이터라고 판단하는 것이다.

 

여기서 Signature의 비밀키(Secret Key)값은 오직 서버만 알고 있어야 하기 때문에 서버에 저장해 놓는다.

 

JWT 단점 및 고려사항

  • Self-contained : 토큰 자체에 정보를 담고 있으므로 양날의 검이 될 수 있다.
  • 토큰 길이 : 토큰의 Payload에 3종류의 클레임을 저장하기 때문에 정보가 많아질수록 토큰의 길이가 늘어나 네트워크에 부하를 줄 수 있다.
  • Payload 인코딩 : Payload 자체는 암호화된 것이 아니라 base 64로 인코딩된 것이다. 중간에 Payload를 탈취하여 디코딩하면 데이터를 볼 수 있으므로, JWE로 암호화하거나 Payload에 중요 데이터를 넣어서는 안된다.
  • Stateless : JWT는 상태를 저장하지 않기 때문에 한 번 만들어지면 제어가 불가능하다. 즉, 토큰을 임의로 삭제하는 것이 불가능하므로 토큰 만료 시간을 꼭 넣어주어야 한다.
  • Store Token : 토큰은 클라이언트 측에서 관리해야 하기 때문에, 토큰을 저장해야 한다.

 

아래의 주소로 들어가서 JWT가 제대로 만들어진 토큰인지 확인해 볼 수 있다.

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

[그림 7] jwt 인코딩, 디코딩이 가능한 사이트

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함