[python] json형식 로그 해싱, 암호화/복호화 하기

ggydo59·2023년 3월 22일
0

💡 DRF로 구축한 백엔드에 클라이언트가 CRUD 요청할 때마다 로그를 남기도록 코딩을 해놨는데요, 주요 사용자 정보는 해싱하고, 다른정보들은 암호화하고 압축하여 Amazon S3에 적재했습니다. 그 과정을 기록했습니다.

해싱(Hashing)

Introduction to Salted-Hashed Passwords

개념

해싱은 단순히 특정 문자열을 어떤 함수를 통과시켜서 다른 문자열로 바꾸는 것을 의미합니다.(되돌릴 수 없음)
그래서 사용자의 패스워드에 많이 사용되었는데요, 문제는 같은 비밀번호를 사용하는 경우에 같은 해싱 비밀번호를 가진다는 것입니다.

그래서 도입된 것이 Salt-Hashed Password 입니다.

Salted-Hash = SHA256(password + salt)

와 같은 방식인데요(SHA256은 해싱 알고리즘을 의미합니다. 해시의 의미를 잘생각보면 됩니다. 함수같은느낌?)

패스워드마다 salt로 칭하는 짧은 문자열을 넣어서 해싱함으로써 같은 비밀번호도 다른 해시값이 나오게 됩니다.

주의할 점

Salt라고 불리는 문자열이 같은 문자열에 재사용되는 것을 주의해야합니다. Salt를 첨가 한 것이 의미도 없이 같은 해시값이 나올테니까요.

너무 짧은 Salt문자열은 피해야합니다. 예를 들어 한 문자이면 완전 탐색을 통해 패스워드를 알아낼 수도 있으니까요.

도입

저는 hashlib를 통해 구현했습니다. Salt는 랜덤한 바이너리 값으로 붙여주어서 중요하다고 여길 수 있는 유저의 정보를 해시함수를 통과시켜서 다른 문자열로 바꾸었습니다.


def get_hash(integer):
    """
    입력한 정수값을 binary 문자열로 인코딩 후, salt값을 append하여 SHA256 알고리즘을 이용하여 해시값을 생성하는 함수
    """

    salt = os.urandom(32) #binary 값 생성
    #ex)\xa9\x84\x01\x96\xa4\t\xadP\x1e\xf3:\x94[\xb7\x9c=\xfebI\x03\xa2\x05\xd5\x9a\x19\x9b\xabhO\x13\xb8\x83
    # 

    plainstring = str(integer)
    plaintext = plainstring.encode() #encode하고 싶은 문자열을 binary 문자열로 인코딩
    digest = hashlib.pbkdf2_hmac('sha256', plaintext, salt, 10000) #digest 객체는 생성된 해시값을 가짐
    hex_hash = digest.hex()
    return hex_hash #바이트 문자열을 16진수로 변환한 문자열(hex)을 반환

암호화/복호화(encrypt/decrypt)

python의 cryptographyFernet을 활용하여 대칭키 암호화/복호화를 구현했습니다.

여기서 암호화 방식에는 대칭키 방식과 비대칭키 방식이 있는데요, 간단히 설명드리자면 대칭키는 암/복호화 하는 키가 같은 키가 필요하고, 비대칭키는 암호화하는 키와 복호화하는 키가 다른 암호화 방식을 의미합니다. 자세한 건 아래 참고한 블로그를 확인해주세요.

구현

  • 생성한 로그들

  • 최초로 RestAPI 방식으로 요청한 로그를 기록한 것은 board_logging.log입니다. 게시판에 글을 쓰고 수정하고 삭제하는 요청을 기록을 했습니다.

  • 대략 아래와 같이 기록됩니다. (user_id 는 해싱을 거친 상태입니다. 원래는 1과같은 정수로 나타내집니다.)

  • 이러한 json 데이터를 암호화 했습니다.

  • 코드

    import hashlib
    import os
    from cryptography.fernet import Fernet
    from pathlib import Path
    from dateutil import parser #epoch time 생성
    import json
    from uuid import uuid4 #recordId 생성
    
    def decrypt(plaintext):
        """
        양방향 암호화를 사용하여 key를 생성 및 별도 파일에 저장하며, 복호화된 데이터를 반환하는 함수
        plaintext: 복호화하려는 데이터(json 형태)
        """
        #logkey.key 파일에서 key값 불러오기
        mod_path = Path(__file__).parent
        #print(mod_path)
        absolute_keyfile_path = (mod_path/"./logkey.key").resolve()#resolve: 절대 경로 반환
        
        #print(absolute_keyfile_path)
        my_file = Path(absolute_keyfile_path)
        if my_file.is_file():
            # 'logkey.key' 파일이 존재
            with open(absolute_keyfile_path,'rb') as file:
                key = file.read()    
        
        fernet = Fernet(key)
        json_log = plaintext
        decrypt_str = fernet.decrypt(f"{json_log}".encode('ascii'))
        # decrypt_str = fernet.decrypt(encrypt_str)
        return decrypt_str.decode('utf-8')
    
    def encrypt(plaintext):
        """
        양방향 암호화를 사용하여 key를 생성 및 별도 파일에 저장하며, 암호화된 데이터를 반환하는 함수
        plaintext: 암호화하려는 데이터(json 형태)
        """
        #logkey.key 파일에서 key값 불러오기
        mod_path = Path(__file__).parent
        absolute_keyfile_path = (mod_path /"./logkey.key").resolve() #resolve: 절대 경로 반환
        print(absolute_keyfile_path)
        my_file = Path(absolute_keyfile_path)
        if my_file.is_file():
            # 'logkey.key' 파일이 존재
            with open(absolute_keyfile_path,'rb') as file:
                key = file.read()
        else:
            #키 생성, 'logkey.key' 파일 생성 및 키값 저장
            key = Fernet.generate_key()
            with open('logkey.key','wb') as file:
                file.write(key)
        
        fernet = Fernet(key)
        json_log = plaintext
        encrypt_str = fernet.encrypt(f"{json_log}".encode('ascii'))
        # decrypt_str = fernet.decrypt(encrypt_str)
        return encrypt_str
    def executedata_from_encrypted_log():
        """
        암호화된 로그로부터 복호화 하여 메타데이터 추출
        """
        mod_path = Path(__file__).parent
        decrypt_log_path = (mod_path/"../logs/decrypt_log.json").resolve()
        absolute_logfile_path = (mod_path /"../logs/encrypted_log.json").resolve()
        print(absolute_logfile_path)
        
        with open(absolute_logfile_path,'r') as file:
                meta =  [json.loads(decrypt(line["data"])) for line in json.loads(file.read())]
                      
                #print(type(file_data))
                # Join encrypted with file_data inside encrypted_logs
                
                # Sets file's current position at offset.
                #file.seek(0)
                # convert back to json.
        with open(decrypt_log_path, 'w') as file:
              file.write(json.dumps(meta, indent=2))            
        
    
    def update_file():
        """
        로그 파일에 로그가 추가될 때마다 해당 내용을 가져와서 암호화된 내용을 별도 JSON 파일에 저장하는 함수
        recordId, ArrivalTimestamp 생성 후 암호화된 데이터 (data)와 함께 저장
        """
        #로그 파일 읽어오기
        mod_path = Path(__file__).parent
        
        absolute_logfile_path = (mod_path /"../logs/board_logging.log").resolve()
    
        #로그 파일의 마지막 줄 읽어오기
        with open(absolute_logfile_path, 'rb') as f:
            try:  # catch OSError in case of a one line file 
                f.seek(-2, os.SEEK_END)
                while f.read(1) != b'\n':
                    f.seek(-2, os.SEEK_CUR)
            except OSError:
                f.seek(0)
            last_line = f.readline().decode()
    
        #데이터 생성하기
        data = encrypt(last_line).decode('utf8')
        data_dict = json.loads(last_line) #json 형태의 log를 dictionary 형태로 변환
        strtime = data_dict["time"]
        epoch_time = parser.parse(strtime).timestamp() #string 형태의 로그 생성 시간을 timestamp 형태로 변환
    
        encrypted = {}
        encrypted["recordId"] = uuid4().int  #랜덤한 고유값
        encrypted["ArrivalTimestamp"] = epoch_time  #로그 생성 시간 (epoch time으로 표시)
        encrypted["data"] = data  #암호화된 개별 로그 데이터값
    
        newLogfile_path = Path(absolute_logfile_path).parent
        newLogfile_path = (newLogfile_path /"encrypted_log.json").resolve() #logs 폴더에 저장
    
        # json_root = {"encrypted_logs": []} #json 파일 생성 시 필요한 root 추가
        # json_root = json.dumps(json_root, indent=4)
    
        
        my_file = Path(newLogfile_path)
        if not my_file.is_file():
            with open(newLogfile_path,'w') as file:
                    file.write('[')
                    file.write(json.dumps(encrypted, indent=2))                             
                    file.write(']')
        else:
            with open(newLogfile_path,'r+') as file:
                file_data = json.loads(file.read())
                #print(type(file_data))
                # Join encrypted with file_data inside encrypted_logs
                file_data.append(encrypted)
                # Sets file's current position at offset.
                file.seek(0)
                # convert back to json.
                file.write(json.dumps(file_data, indent=2))
  • 흐름을 설명드리면, 매 요청 마다 로그가 남게되고 그 로그는 로그파일의 마지막줄에 기록됩니다. 마지막줄을 읽어서 암호화한 다음 UUID와 데이터를 생성한 시간을 epoxytime으로 바꾸고, data 부분에 암호화한 데이터 파일이 쓰여집니다. 결과물은 아래와 같습니다.

  • 로그 한줄 당 이러한 형태로 기록이 됩니다.

  • 실행하고 나면 아래와 같은 logkey.key가 생성이 되는데요

  • 이 파일을 통해 암호화 / 복호화가 이루어집니다. 안의 파일을 열어보면 키로 사용되는 문자열이 들어있습니다.

  • 이 과정을 통해 json 파일 데이터를 다루는 방법에 조금은 익숙해졌습니다. 사실 json 데이터를 python에서 다루는 과정이 제일 오래걸렸던 것 같습니다ㅠ 이렇게 다시 로그파일을 복호화해서 ELK 스택을 구현해보려합니다. 지금까지 읽어주셔서 감사합니다.

  • 전체 코드는 여기서 참고 해주세요

    ETL_pipeline/board at master · ggydo59/ETL_pipeline

참고

profile
데이터엔지니어입니다.

0개의 댓글