더북(TheBook)

암호화와 복호화를 담당하는 이 두 함수를 이용하면 그 어떤 데이터라도 쉽게 암호화하여 데이터를 저장하고 다시 복호화 할 수 있을 것이다. 또한 함수 형태로 만들었으므로 어떤 테이블에 있는 어느 컬럼이라도 암호화와 복호화를 적용할 수 있다. 지금까지 작성한 MY_UTIL_PKG의 전체 소스를 정리하면 다음과 같다.

입력

    CREATE OR REPLACE PACKAGE my_util_pkg IS
        -- 1. 프로그램 소스 검색 프로시저
        PROCEDURE program_search_prc (ps_src_text IN VARCHAR2);

        -- 2. 객체검색 프로시저
        PROCEDURE object_search_prc (ps_obj_name IN VARCHAR2);

        -- 3. 테이블 Layout 출력
        PROCEDURE table_layout_prc ( ps_table_name IN VARCHAR2);

        -- 4. 컬럼 값을 세로로 출력
        PROCEDURE print_col_value_prc ( ps_query IN VARCHAR2 );

        -- 이메일 전송과 관련된 패키지 상수
        pv_host VARCHAR2(10)   := 'localhost';  -- SMTP 서버명
        pn_port NUMBER         := 25;           -- 포트번호
        pv_domain VARCHAR2(30) := 'hong.com';   -- 도메인명

        pv_boundary VARCHAR2(50) := 'DIFOJSLKDWFEFO.WEFOWJFOWE';  -- boundary text
        pv_directory VARCHAR2(50) := 'SMTP_FILE'; --파일이 있는 디렉토리명

        -- 5. 이메일 전송
        PROCEDURE email_send_prc ( ps_from    IN VARCHAR2,
                                   ps_to      IN VARCHAR2,
                                   ps_subject IN VARCHAR2,
                                   ps_body    IN VARCHAR2,
                                   ps_content IN VARCHAR2  DEFAULT 'text/plain;',
                                   ps_file_nm IN VARCHAR2
                                 );

        -- 6. 비밀번호 생성
        FUNCTION fn_create_pass ( ps_input IN VARCHAR2,
                                  ps_add   IN VARCHAR2 )
                 RETURN RAW;

        -- 7. 비밀번호 확인
        FUNCTION fn_check_pass ( ps_input IN VARCHAR2,
                                 ps_add   IN VARCHAR2,
                                 p_raw    IN RAW )
                 RETURN VARCHAR2;

        -- 8. 암호화 함수
        FUNCTION fn_encrypt ( ps_input_string IN VARCHAR2 )
                 RETURN RAW;

        -- 9. 복호화 함수
        FUNCTION fn_decrypt ( prw_encrypt IN RAW )
                 RETURN VARCHAR2;

    END my_util_pkg;

    -- 패키지 본문
    CREATE OR REPLACE PACKAGE BODY my_util_pkg IS

      /* 1. 프로그램 소스 검색 프로시저 *************************************************************************************/
      PROCEDURE program_search_prc (ps_src_text IN VARCHAR2)
      IS
        vs_search VARCHAR2(100);
        vs_name   VARCHAR2(1000);
      BEGIN
        -- 찾을 키워드 앞뒤에 '%'를 붙인다.
        vs_search := '%' || NVL(ps_src_text, '%') || '%';

        -- dba_source에서 입력된 키워드로 소스를 검색한다.
        -- 입력 키워드가 대문자 혹은 소문자가 될 수 있으므로 UPPER, LOWER 함수를 이용해 검색한다.
        FOR C_CUR IN ( SELECT name, type, line, text
                         FROM user_source
                        WHERE text like UPPER(vs_search)
                           OR text like LOWER(vs_search)
                        ORDER BY name, type, line
                      )
        LOOP
           -- 프로그램 이름과 줄번호를 가져와 출력한다.
           vs_name := C_CUR.name || ' - ' || C_CUR.type || ' - ' || C_Cur.line || ' : ' || REPLACE(C_CUR.text, CHR(10), '');
           DBMS_OUTPUT.PUT_LINE( vs_name);
        END LOOP;

      END program_search_prc;

      /* 2. 객체검색 프로시저 *************************************************************************************************/
      PROCEDURE object_search_prc (ps_obj_name IN VARCHAR2)
      IS
        vs_search VARCHAR2(100);
        vs_name   VARCHAR2(1000);
      BEGIN
        -- 찾을 키워드 앞뒤에 '%'를 붙인다.
        vs_search := '%' || NVL(ps_obj_name, '%') || '%';

        -- referenced_name 입력된 키워드로 참조객체를 검색한다.
        -- user_dependencies에는 모두 대문자로 데이터가 들어가 있으므로 UPPER 함수를 이용해 검색한다.
        FOR C_CUR IN ( SELECT name, type
                         FROM user_dependencies
                        WHERE referenced_name LIKE UPPER(vs_search)
                        ORDER BY name, type
                      )
        LOOP
           -- 프로그램 이름과 줄번호를 가져와 출력한다.
           vs_name := C_CUR.name || ' - ' || C_CUR.type ;
           DBMS_OUTPUT.PUT_LINE( vs_name);
        END LOOP;

      END object_search_prc;

      /* 3. 테이블 Layout 출력 ***********************************************************************************************/
      PROCEDURE table_layout_prc ( ps_table_name IN VARCHAR2)
      IS
        vs_table_name VARCHAR2(50) := UPPER(ps_table_name);
        vs_owner      VARCHAR2(50);
        vs_columns    VARCHAR2(300);
      BEGIN
      BEGIN
        -- TABLE이 있는지 검색
        SELECT OWNER
          INTO vs_owner
          FROM ALL_TABLES
         WHERE TABLE_NAME = vs_table_name;

      EXCEPTION WHEN NO_DATA_FOUND THEN
           DBMS_OUTPUT.PUT_LINE(vs_table_name || '라는 테이블이 존재하지 않습니다');
           RETURN;
        END;

        DBMS_OUTPUT.PUT_LINE('-------------------------------------------------------');
        DBMS_OUTPUT.PUT_LINE('테이블: ' || vs_table_name || ' , 소유자 : ' || vs_owner);
        DBMS_OUTPUT.PUT_LINE('-------------------------------------------------------');

      -- 컬럼정보 검색 및 출력
        FOR C_CUR IN ( SELECT column_name, data_type, data_length, nullable, data_default
                         FROM ALL_TAB_COLS
                        WHERE table_name = vs_table_name
                        ORDER BY column_id
                      )
        LOOP
           -- 컬럼 정보를 출력한다. 줄을 맞춰 출력되도록 RPAD 함수를 사용한다.
           vs_columns := RPAD(C_CUR.column_name, 20) || RPAD(C_CUR.data_type, 15) || RPAD(C_CUR.data_length, 5) || RPAD(C_CUR.nullable, 2) || RPAD(C_CUR.data_default, 10);
           DBMS_OUTPUT.PUT_LINE( vs_columns);
        END LOOP;

      END table_layout_prc;


      /* 4. 컬럼 값을 세로로 출력 *****************************************************************************************************************************************/
      PROCEDURE print_col_value_prc ( ps_query IN VARCHAR2 )
      IS
          l_theCursor     INTEGER DEFAULT DBMS_SQL.OPEN_CURSOR;
          l_columnValue   VARCHAR2(4000);
          l_status        INTEGER;
          l_descTbl       DBMS_SQL.DESC_TAB;
          l_colCnt        NUMBER;
      BEGIN
          -- 쿼리구문이 p_query 매개변수에 들어오므로 이를 파싱한다.
          DBMS_SQL.PARSE(  l_theCursor,  ps_query, DBMS_SQL.NATIVE );

          -- DESCRIBE_COLUMN 프로시저 : 커서에 대한 컬럼정보를 DBMS_SQL.DESC_TAB 형 변수에 넣는다.
          DBMS_SQL.DESCRIBE_COLUMNS  ( l_theCursor, l_colCnt, l_descTbl );

          -- 선택된 컬럼 개수만큼 루프를 돌며 DEFINE_COLUMN 프로시저를 호출해 컬럼을 정의한다.
          FOR i IN 1..l_colCnt
          LOOP
              DBMS_SQL.DEFINE_COLUMN (l_theCursor, i, l_columnValue, 4000);
          END LOOP;

          -- 실행
          l_status := DBMS_SQL.EXECUTE(l_theCursor);

          WHILE ( DBMS_SQL.FETCH_ROWS (l_theCursor) > 0 )
          LOOP
              -- 컬럼 개수만큼 다시 루프를 돌면서 컬럼 값을 l_columnValue 변수에 담는다.
              -- DBMS_SQL.DESC_TAB 형 변수인 l_descTbl.COL_NAME은 컬럼 명칭이 있고
              -- l_columnValue에는 컬럼 값이 들어있다.
              FOR i IN 1..l_colCnt
              LOOP
                DBMS_SQL.COLUMN_VALUE ( l_theCursor, i, l_columnValue );
                DBMS_OUTPUT.PUT_LINE  ( rpad( l_descTbl(i).COL_NAME, 30 ) || ': ' || l_columnValue );
              END LOOP;
              DBMS_OUTPUT.PUT_LINE( '-----------------' );
          END LOOP;

          DBMS_SQL.CLOSE_CURSOR (l_theCursor);

      END print_col_value_prc;

      /* 5. 이메일 전송 **************************************************************************************************************************/
      PROCEDURE email_send_prc ( ps_from    IN VARCHAR2,  -- 보내는 사람
                                 ps_to      IN VARCHAR2,  -- 받는 사람
                                 ps_subject IN VARCHAR2,  -- 제목
                                 ps_body    IN VARCHAR2,  -- 본문
                                 -- Content-Type
                                 ps_content IN VARCHAR2  DEFAULT 'text/plain;',
                                 ps_file_nm IN VARCHAR2   -- 첨부파일
                               )
      IS
        vc_con utl_smtp.connection;

        v_bfile        BFILE;       -- 파일을 담을 변수
        vn_bfile_size  NUMBER := 0; -- 파일크기

        v_temp_blob    BLOB := EMPTY_BLOB; -- 파일을 옮겨담을 BLOB 타입 변수
        vn_blob_size   NUMBER := 0;        -- BLOB 변수 크기
        vn_amount      NUMBER := 54;       -- 54 단위로 파일을 잘라 메일에 붙이기 위함
        v_tmp_raw      RAW(54);            -- 54 단위로 자른 파일내용이 담긴 RAW 타입변수
        vn_pos         NUMBER := 1; --파일 위치를 담는 변수

      BEGIN

        vc_con := UTL_SMTP.OPEN_CONNECTION(pv_host, pn_port);

        UTL_SMTP.HELO(vc_con, pv_domain); -- HELO
        UTL_SMTP.MAIL(vc_con, ps_from);   -- 보내는사람
        UTL_SMTP.RCPT(vc_con, ps_to);     -- 받는사람

        UTL_SMTP.OPEN_DATA(vc_con); -- 메일본문 작성 시작
        UTL_SMTP.WRITE_DATA(vc_con,'MIME-Version: 1.0' || UTL_TCP.CRLF ); -- MIME 버전

        UTL_SMTP.WRITE_DATA(vc_con,'Content-Type: multipart/mixed; boundary="' || pv_boundary || '"' || UTL_TCP.CRLF);
        UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_RAW.CAST_TO_RAW('From: ' || ps_from || UTL_TCP.CRLF) ); -- 보내는사람
        UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_RAW.CAST_TO_RAW('To: ' || ps_to || UTL_TCP.CRLF) );   -- 받는사람
        UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_RAW.CAST_TO_RAW('Subject: ' || ps_subject || UTL_TCP.CRLF) ); -- 제목
        UTL_SMTP.WRITE_DATA(vc_con, UTL_TCP.CRLF );  -- 한 줄 띄우기

        -- 메일 본문
        UTL_SMTP.WRITE_DATA(vc_con, '--' || pv_boundary || UTL_TCP.CRLF );
        UTL_SMTP.WRITE_DATA(vc_con, 'Content-Type: ' || ps_content || UTL_TCP.CRLF );
        UTL_SMTP.WRITE_DATA(vc_con, 'charset=euc-kr' || UTL_TCP.CRLF );
        UTL_SMTP.WRITE_DATA(vc_con, UTL_TCP.CRLF );
        UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_RAW.CAST_TO_RAW(ps_body || UTL_TCP.CRLF)  );
        UTL_SMTP.WRITE_DATA(vc_con, UTL_TCP.CRLF );

        -- 첨부파일이 있다면 ...
        IF ps_file_nm IS NOT NULL THEN

            UTL_SMTP.WRITE_DATA(vc_con, '--' || pv_boundary || UTL_TCP.CRLF );
            -- 파일의 Content-Type은 application/octet-stream
            UTL_SMTP.WRITE_DATA(vc_con,'Content-Type: application/octet-stream; name="' || ps_file_nm || '"' || UTL_TCP.CRLF);
            UTL_SMTP.WRITE_DATA(vc_con,'Content-Transfer-Encoding: base64' || UTL_TCP.CRLF);
            UTL_SMTP.WRITE_DATA(vc_con,'Content-Disposition: attachment; filename="' || ps_file_nm || '"' || UTL_TCP.CRLF);

            UTL_SMTP.WRITE_DATA(vc_con, UTL_TCP.CRLF);

            -- 파일처리 시작
            -- 파일을 읽어 BFILE 변수인 v_bfile에 담는다.
            v_bfile := BFILENAME(pv_directory, ps_file_nm);
            -- v_bfile 담은 파일을 읽기전용으로 연다.
            DBMS_LOB.OPEN(v_bfile, DBMS_LOB.LOB_READONLY);
            -- v_bfile에 담긴 파일의 크기를 가져온다.
            vn_bfile_size := DBMS_LOB.GETLENGTH(v_bfile);

            -- v_bfile를 BLOB 변수인 v_temp_blob에 담기 위해 초기화
            DBMS_LOB.CREATETEMPORARY(v_temp_blob, TRUE);
            -- v_bfile에 담긴 파일을 v_temp_blob 로 옮긴다.
            DBMS_LOB.LOADFROMFILE(v_temp_blob, v_bfile, vn_bfile_size);
            -- v_temp_blob의 크기를 구한다.
            vn_blob_size := DBMS_LOB.GETLENGTH(v_temp_blob);

            -- vn_pos 초기값은 1, v_temp_blob 크기보다 작은 경우 루프
            WHILE vn_pos < vn_blob_size
            LOOP
                -- v_temp_blob에 담긴 파일을 vn_amount(54)씩 잘라  v_tmp_raw에 담는다.
                DBMS_LOB.READ(v_temp_blob, vn_amount, vn_pos, v_tmp_raw);
                -- 잘라낸 v_tmp_raw를 메일에 첨부한다.
                UTL_SMTP.WRITE_RAW_DATA(vc_con, UTL_ENCODE.BASE64_ENCODE ( v_tmp_raw));
                UTL_SMTP.WRITE_DATA(vc_con,  UTL_TCP.CRLF );

                v_tmp_raw := NULL;
                vn_pos := vn_pos + vn_amount;
            END LOOP;

          DBMS_LOB.FREETEMPORARY(v_temp_blob); -- v_temp_blob 메모리 해제
          DBMS_LOB.FILECLOSE(v_bfile); -- v_bfile 닫기

        END IF; -- 첨부파일 처리 종료

        -- 맨 마지막 boundary에는 앞과 뒤에 '--'를 반드시 붙여야 한다.
        UTL_SMTP.WRITE_DATA(vc_con, '--' ||  pv_boundary || '--' || UTL_TCP.CRLF );

        UTL_SMTP.CLOSE_DATA(vc_con); -- 메일 본문 작성 종료
        UTL_SMTP.QUIT(vc_con);       -- 메일 세션 종료

      EXCEPTION
        WHEN UTL_SMTP.INVALID_OPERATION THEN
             dbms_output.put_line(' Invalid Operation in Mail attempt using UTL_SMTP.');
             dbms_output.put_line(sqlerrm);
             UTL_SMTP.QUIT(vc_con);
        WHEN UTL_SMTP.TRANSIENT_ERROR THEN
             dbms_output.put_line(' Temporary e-mail issue - try again');
             UTL_SMTP.QUIT(vc_con);
        WHEN UTL_SMTP.PERMANENT_ERROR THEN
             dbms_output.put_line(' Permanent Error Encountered.');
             dbms_output.put_line(sqlerrm);
             UTL_SMTP.QUIT(vc_con);
        WHEN OTHERS THEN
           dbms_output.put_line(sqlerrm);
           UTL_SMTP.QUIT(vc_con);

      END email_send_prc;

      /* 6. 비밀번호 생성 **************************************************************************************************************************/
      FUNCTION fn_create_pass ( ps_input IN VARCHAR2,
                                ps_add   IN VARCHAR2 )
               RETURN RAW
      IS
        v_raw     RAW(32747);
        v_key_raw RAW(32747);
        v_input_string VARCHAR2(100);
      BEGIN
        -- 키 값을 가진 ch19_wrap_pkg 패키지의 pv_key_string 상수를 가져와 RAW 타입으로 변환한다.
        v_key_raw := UTL_RAW.CAST_TO_RAW(ch19_wrap_pkg.pv_key_string );

        -- 좀 더 보안을 강화하기 위해 두 개의 입력 매개변수와 특수문자인 $%를 조합해
        -- MAC 함수의 첫 번째 매개변수로 넘긴다.
        v_input_string := ps_input || '$%' || ps_add;

        -- MAC 함수를 사용해 입력 문자열을 RAW 타입으로 변환한다.
        v_raw := DBMS_CRYPTO.MAC (src => UTL_RAW.CAST_TO_RAW(v_input_string)
                                 ,typ => DBMS_CRYPTO.HMAC_SH1
                                 ,key => v_key_raw);

        RETURN v_raw;
      END fn_create_pass;

      /* 7. 비밀번호 확인 **************************************************************************************************************************/
      FUNCTION fn_check_pass ( ps_input IN VARCHAR2,
                               ps_add   IN VARCHAR2,
                               p_raw    IN RAW )
               RETURN VARCHAR2
      IS
        v_raw     RAW(32747);
        v_key_raw RAW(32747);
        v_input_string VARCHAR2(100);

        v_rtn VARCHAR2(10) := 'N';
      BEGIN
        -- 키 값을 가진 ch19_wrap_pkg 패키지의 pv_key_string 상수를 가져와 RAW 타입으로 변환한다.
        v_key_raw := UTL_RAW.CAST_TO_RAW(ch19_wrap_pkg.pv_key_string );

        -- 좀 더 보안을 강화하기 위해 두 개의 입력 매개변수와 특수문자인 $%를 조합해
        -- MAC 함수의 첫 번째 매개변수로 넘긴다.
        v_input_string := ps_input || '$%' || ps_add;

        -- MAC 함수를 사용해 입력 문자열을 RAW 타입으로 변환한다.
        v_raw := DBMS_CRYPTO.MAC (src => UTL_RAW.CAST_TO_RAW(v_input_string)
                                 ,typ => DBMS_CRYPTO.HMAC_SH1
                                 ,key => v_key_raw);

        IF v_raw = p_raw THEN
           v_rtn := 'Y';
        ELSE
           v_rtn := 'N';
        END IF;

        RETURN v_rtn;
      END fn_check_pass;

      /* 8. 암호화 함수 **************************************************************************************************************************/
      FUNCTION fn_encrypt ( ps_input_string IN VARCHAR2 )
               RETURN RAW
      IS
        encrypted_raw RAW(32747);
        v_key_raw RAW(32747);         -- 암호화 키
        encryption_type PLS_INTEGER;  -- 암호화 슈트
      BEGIN
        -- 암호화 키 값을 가져온다.
        v_key_raw := ch19_wrap_pkg.key_bytes_raw;

      -- 암호화 슈트 설정
      encryption_type := DBMS_CRYPTO.ENCRYPT_AES256 + -- 256비트 키를 사용한 AES 암호화
                         DBMS_CRYPTO.CHAIN_CBC +      -- CBC 모드
                         DBMS_CRYPTO.PAD_PKCS5;       -- PKCS5로 이루어진 패딩

        -- ENCRYPT 함수로 암호화를 한다. 매개변수로 들어온 문자열을 UTL_I18N.STRING_TO_RAW를 사용해 RAW 타입으로 변환한다.
        encrypted_raw := DBMS_CRYPTO.ENCRYPT ( src => UTL_I18N.STRING_TO_RAW (ps_input_string, 'AL32UTF8'),
                                               typ => encryption_type,
                                               key => v_key_raw
                                              );

         RETURN encrypted_raw;
      END fn_encrypt;

      /* 9. 복호화 함수 **************************************************************************************************************************/
      FUNCTION fn_decrypt ( prw_encrypt IN RAW )
               RETURN VARCHAR2
      IS
        vs_return VARCHAR2(100);
        v_key_raw RAW(32747);         -- 암호화 키
        encryption_type PLS_INTEGER;  -- 암호화 슈트
        decrypted_raw   RAW (2000);   -- 복호화 데이터
      BEGIN
        -- 암호화 키 값을 가져온다.
        v_key_raw := ch19_wrap_pkg.key_bytes_raw;

      -- 암호화 슈트 설정
      encryption_type := DBMS_CRYPTO.ENCRYPT_AES256 + -- 256비트 키를 사용한 AES 암호화
                         DBMS_CRYPTO.CHAIN_CBC +      -- CBC 모드
                         DBMS_CRYPTO.PAD_PKCS5;       -- PKCS5로 이루어진 패딩

        -- 매개변수로 들어온 RAW 타입 데이터를 다시 복호화 ( 암호화했던 키와 암호화 슈트는 동일하게 사용해야 한다. )
        decrypted_raw := DBMS_CRYPTO.DECRYPT ( src => prw_encrypt,
                                               typ => encryption_type,
                                               key => v_key_raw
                                             );
         -- 복호화된 RAW 타입 데이터를 UTL_I18N.RAW_TO_CHAR를 사용해 다시 VARCHAR2로 변환
         vs_return := UTL_I18N.RAW_TO_CHAR (decrypted_raw, 'AL32UTF8');

         RETURN vs_return;
      END fn_decrypt;

    END my_util_pkg;

지금까지 이 책에서 배운 내용을 토대로 유용하게 쓸 수 있는 몇 가지 프로그램을 만들었다. 독자분들도 평소에 이런 기능을 구현한 프로그램이 있었으면 좋겠다고 생각한 것이 있다면 실제로 구현해서 사용해 보길 바란다. 덧붙어 여기에서 만들었던 프로그램을 약간 수정해 응용하면 훨씬 더 사용하기 쉽고 유용한 프로그램을 만들 수 있을 것이다.

신간 소식 구독하기
뉴스레터에 가입하시고 이메일로 신간 소식을 받아 보세요.