SQLAlchemy와 cryptography로 민감한 데이터 암호화하기

2021.08.19

소프트웨어 의료기기를 합법적으로 제공하기 위해서 수준 높은 사이버보안을 충족시켜야합니다. 여러가지 요구사항 중에 환자정보의 보호를 위해 암호화를 사용해야 한다는 조항이 있습니다. 즉, 환자의 식별자, 나이, 이름, 생체정보는 기록 시점에서 암호화가 되어 저장되어야 하고, 환자 데이터를 참조하거나 계산하는 시점에서 바로 복호화가 되어야 합니다.

라인웍스는 이를 해결하기 위해 SQLAlchemy-Utils 을 사용하였습니다.

SQLAlchemy-Utils 이란?

SQLAlchemy-Utils는 SQLAlchemy 사용자를 위한 맞춤형 데이터 타입과 다양한 보조 함수를 제공합니다. 이 중에 특히 유용한 데이터 타입은 EncryptedType 으로, 다양한 기본 자료형을 암호화할 수 있게 도와줍니다.

왜 데이터베이스의 암호화 기능을 사용하지 않는가?

데이터베이스마다 다양한 암호 알고리즘을 제공합니다. PostgreSQL 은 pgcrypto 익스텐션을 설치하면 암호화기능을 사용할 수 있습니다. 그러나 소프트웨어 의료기기가 설치될 환경이 항상 특정한 데이터베이스를 사용할 수 있다고 보장할 수 없고, 때로 특정 기관에 설치할때 superuser 권한이 없다면 익스텐션을 설치할 수 없을때도 있습니다. 따라서, 데이터베이스에 종속적이지 않은 방법을 찾고자 했습니다. 

환자 정보 ORM

KISA에서 발간한 개인정보의 암호화 조치 안내서에 따르면 충분한 보안강도를 갖기 위해 112비트 이상의 키를 사용해야 합니다. 따라서 이 기준을 충족하는 AES-128 알고리즘에 길이가 16자인 key를 사용하였습니다. 예시로 환자의 식별자, 이름, 성별, 생년월을 저장할 모델을 만들어 볼 것입니다. 예시라서 key를 노출시켰지만, 실제 제품에 적용할때는 key를 하드코딩하지 않아야 합니다.


import sqlalchemy as sa
from sqlalchemy.orm import declarative_base
from sqlalchemy_utils import EncryptedType
from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine

Base = declarative_base()

key = "1234567812345678"

class Patient(Base):
  __tablename__ = "patient"
  id = sa.Column(sa.Integer, sa.Sequence("pid_seq"), primary_key=True)
  patient_id = sa.Column(EncryptedType(sa.Unicode, key, AesEngine, "pkcs5"))
  fullname = sa.Column(EncryptedType(sa.Unicode, key, AesEngine, "pkcs5"))
  gender = sa.Column(EncryptedType(sa.Unicode, key, AesEngine, "pkcs5"))
  birthm = sa.Column(EncryptedType(sa.Unicode, key, AesEngine, "pkcs5"))

  def __repr__(self):
    return f""

암호화 예시

데이터 입력

테스트로 임의의 환자이름을 입력해보겠습니다.


p = Patient(fullname="secret_patient")
db.session.add(p)
db.session.commit()

# Remove all object instances from this Session.
db.session.expunge_all()

결과

로그에서 보이듯이 파이썬 객체에서 문자열을 암호화하여 테이블에 저장될 때는 바이너리로 전달되는 것을 볼 수 있습니다.

테이블을 직접 조회해볼때에도 암호화된 fullname을 확인할 수 있습니다.


INFO sqlalchemy.engine.Engine BEGIN (implicit)
INFO sqlalchemy.engine.Engine INSERT INTO patient (id, patient_id, fullname, gender, birthm) VALUES (nextval(pid_seq'), %(patient_id)s, %(fullname)s, %(gender)s, %(birthm)s) RETURNING patient.id
INFO sqlalchemy.engine.Engine [generated in 0.04377s] {'patient_id': None, 'fullname': , 'gender': None, 'birthm': None}
INFO sqlalchemy.engine.Engine COMMIT

복호화 예시

데이터 출력

앞서 테스트했던 환자이름으로 조회를 해보겠습니다.


instance = db.session.query(Patient)\
  .where(Patient.fullname=="secret_patient").first()
print(instance)

결과

로그에서 보이듯이 테이블에서 읽어올 시점에는 암호화된 바이너리를 받지만, 파이썬 객체로 넘어오면서 복호화 되어 원본으로 핸들링할 수 있게 됩니다.


INFO sqlalchemy.engine.Engine BEGIN (implicit)
INFO sqlalchemy.engine.Engine SELECT patient.id AS patient_id, patient.patient_id AS patient_patient_id, patient.fullname AS patient_fullname, patient.gender AS patient_gender, patient.birthm AS patient_birthm
FROM patient
WHERE patient.fullname = %(fullname_1)s
 LIMIT %(param_1)s
INFO sqlalchemy.engine.Engine [generated in 0.00056s] {'fullname_1': <psycopg2.extensions.Binary object at 0x103830900>, 'param_1': 1}
<User(patient_id=None, fullname='secret_patient', gender='None', birthm='None')>

이 방식의 장점

데이터베이스마다 제공하는 암호화 함수는 사용법이 상이합니다. 따라서 데이터베이스에서 제공하는 암호화 방법을 사용하는 경우, 데이터베이스가 바뀌면 SQLAlchemy에 그에 맞는 dialect, 즉 암호화 함수를 쿼리에서 사용할 수 있도록 컴파일하는 기능을 추가해줘야 합니다. SQLAlchemy-Utils 의 경우 이런 추가 작업 없이 데이터 암호화 할 수 있다는 점이 장점입니다. 

나가며

라인웍스는 소프트웨어 의료기기를 개발하기 위한 다양한 기술을 축적하고 있습니다. 헬스케어 데이터를 기반으로 한 기술과 제품에 관심있는 사람들의 많은 지원을 기다리고 있습니다. 자세한 내용은 라인웍스 홈페이지의 채용 페이지Notion 페이지를 참고하시기 바랍니다.

Cinyoung Hur

Lead Data Engineer, Software Developer

Cinyoung Hur