Inhaltsverzeichnis
Passwörter und ähnliche Schlüssel in PL/SQL Packages schützen
Um zum Beispiel auf ein LDAP Verzeichnis in PL/SQL zuzugreifen, müssen oft Passwörter in PL/SQL Programmen hinterlegt werden, oder bei einer Kreditkartenverschlüsselung der Master Key.
Die einfachste Möglichkeit ist das Schreiben in eine Tabelle.
Für das Deployment einer Applikation ist es aber oft besser, das ganze über den eigentlichen Source Code komplett abzuwickeln als umständliche wieder ein DML Statement irgendwo mit einzubauen, eine Tabelle anzulegen und das dann auch noch zu pflegen.
Das Password darf aber im Source Code, in Backups/Exports und ähnlichen nicht mehr lesbar sein.
Im ersten Schritt heißt das wohl, das wir das Passwort nur verschlüsselt speichern dürfen.
Aber wohin dann mit dem Schlüssel?
Der Algorithmus für die Verschlüsselung kann so aufwendig und ausgefeilt sein wie es nur geht, wenn der Schlüssel und der verschlüsselte Wert letztendlich zusammen gespeichert werden, ist alles hinfällig.
Da aber ein 100% Schutz doch sehr aufwendig werden kann, ist mein Ziel hier nun um einiges niedrier.
Ich will erreichen:
- Kein Klarschrift Passwort im Source Code in Git/SVN oder auf der USB Platte des Entwicklers
- Keine oder geringe Chance das verschlüsselte Kennwort auf einen anderen System wie der USB Platte des Entwicklers wieder einfach nachträglich auszulesen
- Keine festen Schlüssel, die wiederum irgendwo hinterlegt sind, die Schlüssel müssen sich aus der Laufzeitumgebung ergeben - Ändert sich diese, muss das Passwort dann eben neu hinterlegt werden
- Die Möglichkeit in einer Routine diesen dynamischen zentralen Schlüssel nochmals zu verändern um die Logik hinter dem Schlüssel in Source Code möglichst zu verteilen
Zusätzlicher Schutz:
- Das zu schützende Package liegt in einem gesonderten Schema
- ⇒ 12c Möglichkeit nützen das nur das Package explizit auf die eine Routine zu granten die das Package dann auch benötigt, nicht an den eigentlichen Applikationsuser
- ⇒ 12c Feature „accessible by“ ausnützen (d.h. die Routine bzw der Type/die Function kann nur in diesem Package verwandt werden!)
Die alles bietet keine Schutz vor einen „echten“ Angriff!
ABER es erfüllt viele Regularien Buchstaben getreu und verhindert das „versehentliche“ verteilen von Passwörtern und macht den Feierabend Hackern das Leben schwerer (oder interessanter je nach dem .-) )!
Dies einfachen Maßnahmen schützen damit zwar nur zu 90%, sind immer noch besser als der leichtsinnige Umgang mit Passwörtern im Quellcode in Klarschrift!
Bezgl. Scripting siehe auch diesen Ideen dazu Datenbank User Passwörter in Shell und SQL Skripten und um in einen Linux Script das Password zu verschlüsseln, siehe auch Bash Snippets - Passwörter in Configurationen verschlüsseln.
Eine PL/SQL Lösung
Hier wird eine Lösung vorgeschlagen, bei der bewust auf den Einsatz einer Tabelle verzichtet wird, das läßt sich aber trivial einfach in den Code an den entsprechenden Stellen einbauen.
Vorbereitung
Dem Package Owner muss das Rechte gebenen werden:
- grant create procedure to <package_owner>
- grant create type to <package_owner>
Die Spezifikation:
- encrypt_util_spec.sql
CREATE OR REPLACE PACKAGE encrypt_util AS crypt_value_error EXCEPTION; PRAGMA exception_init(crypt_value_error, -06502); crypt_key_error EXCEPTION; PRAGMA exception_init(crypt_key_error, -28817); --+ ----------------------------------------------------------------------- -- encrypt text --+ ----------------------------------------------------------------------- FUNCTION encrypt (p_plaintext VARCHAR2) RETURN RAW deterministic; --+ ----------------------------------------------------------------------- -- decrypt text --+ ----------------------------------------------------------------------- FUNCTION decrypt (p_encryptedtext RAW) RETURN VARCHAR2 deterministic; --+ ----------------------------------------------------------------------- -- store passwords in to a object --+ ----------------------------------------------------------------------- PROCEDURE storepwd (p_pwd VARCHAR2 , p_slot NUMBER , p_store VARCHAR2 DEFAULT 'OBJECT' , p_private_key VARCHAR2 DEFAULT 'NO' ); --+ ----------------------------------------------------------------------- -- get User password --+ ----------------------------------------------------------------------- FUNCTION getuserpwd (p_slot NUMBER,p_private_key VARCHAR2 DEFAULT 'NO') RETURN VARCHAR2; END encrypt_util; /
Body:
- encrypt_util_body.sql
CREATE OR REPLACE PACKAGE BODY encrypt_util AS --+ ---------------------------------------------------------------------------- -- thankt to: -- see http://www.oracleflash.com/41/Encrypt-or-Decrypt-sensitive-data-using-PLSQL---DBMS_CRYPTO.html --+ -------------------------------------------------------------------------- g_crypt_clear_key VARCHAR2 (256) := ''; g_encryption_key RAW (32) := ''; --+ ---------------------------------------------------------------------------- -- ENCRYPT_DES is the encryption algorithem. -- Data Encryption Standard. Block cipher. -- Uses key length of 56 bits. -- -- CHAIN_CBC Cipher Block Chaining. Plaintext is XORed with the previous ciphertext -- block before it is encrypted. -- -- PAD_PKCS5 Provides padding which complies with the PKCS #5: Password-Based -- Cryptography Standard --+ ---------------------------------------------------------------------------- g_encryption_type PLS_INTEGER := dbms_crypto.encrypt_des + dbms_crypto.chain_cbc + dbms_crypto.pad_pkcs5; --+ ---------------------------------------------------------------------------- -- encrypt a text --+ ---------------------------------------------------------------------------- FUNCTION encrypt (p_plaintext VARCHAR2) RETURN RAW deterministic IS encrypted_raw RAW (2000); BEGIN encrypted_raw := dbms_crypto.encrypt (src => UTL_RAW.cast_to_raw (p_plaintext) , typ => g_encryption_type , key => g_encryption_key ); RETURN encrypted_raw; END encrypt; --+ ---------------------------------------------------------------------------- -- decrypt a text --+ ---------------------------------------------------------------------------- FUNCTION decrypt (p_encryptedtext RAW) RETURN VARCHAR2 deterministic IS decrypted_raw RAW (2000); BEGIN decrypted_raw := dbms_crypto.decrypt (src => p_encryptedtext , typ => g_encryption_type , key => g_encryption_key ); RETURN (UTL_RAW.cast_to_varchar2 (decrypted_raw)); END decrypt; --+ ---------------------------------------------------------------------------- -- clean string from whitespaces to avoid sql injection --+ ---------------------------------------------------------------------------- FUNCTION cleantext (p_text VARCHAR2) RETURN VARCHAR2 IS v_text VARCHAR2 (32000); BEGIN v_text := REPLACE (p_text, CHR (10), ''); v_text := REPLACE (v_text, CHR (13), ''); v_text := REPLACE (v_text, ' ', ''); v_text := regexp_replace (v_text , '[[:space:]]' , ''); RETURN v_text; END cleantext; --+ ---------------------------------------------------------------------------- -- get a key from the local db enviroment -- The encryption key for DES algorithem, should be 8 bytes or more. --+ ---------------------------------------------------------------------------- FUNCTION getkeyfromlocal(p_private_key VARCHAR2 DEFAULT NULL) RETURN VARCHAR2 IS v_obj_id1 VARCHAR2 (12) := '00000'; v_obj_id2 VARCHAR2 (12) := '00000'; v_key VARCHAR2 (2000) := 'PWD'; BEGIN -- get some ids from default objects, that not chaning so often .-) SELECT TO_CHAR (object_id) INTO v_obj_id1 FROM user_objects WHERE object_name = 'ENCRYPT_UTIL' AND object_type = 'PACKAGE BODY'; SELECT TO_CHAR (SUM (object_id)) INTO v_obj_id2 FROM all_objects WHERE object_name = 'DBMS_STANDARD' AND owner IN ('SYS', 'PUBLIC'); v_key := INITCAP (SYS_CONTEXT ('USERENV','CURRENT_SCHEMA')) || '#' || LOWER (SYS_CONTEXT ('USERENV', 'CURRENT_SCHEMAID')) || '*' || NVL (v_obj_id1, '00000') || '$' || INITCAP (SYS_CONTEXT ('USERENV', 'DB_NAME')) || '+' || NVL (v_obj_id2, '00000'); IF p_private_key IS NOT NULL THEN v_key := p_private_key||v_key; END IF; RETURN SUBSTR(v_key,0,32); END getkeyfromlocal; --+ ---------------------------------------------------------------------------- -- store passwords in to a object -- need create function right -- grant create procedure to <package_owner> -- grant create type to <package_owner> -- debug exec encrypt_util.storepwd('ABCDE',1,'FUNCTION','1234') --+ ---------------------------------------------------------------------------- PROCEDURE storepwd (p_pwd VARCHAR2 , p_slot NUMBER , p_store VARCHAR2 DEFAULT 'OBJECT' , p_private_key VARCHAR2 DEFAULT 'NO' ) IS CURSOR c_read_type IS SELECT text FROM user_source WHERE name = 'PWD_WALLET' AND TYPE = 'TYPE BODY'; v_f_template VARCHAR2 (2000) := 'create or replace function getSecretUserPwd##Slot## return raw is begin return (''##CRYPT_KEY##''); end;'; v_o_template_s VARCHAR2 (4000) := 'CREATE or replace TYPE pwd_wallet AS OBJECT ( password varchar2(56) ,MEMBER FUNCTION getUserPWD1 RETURN raw ,MEMBER FUNCTION getUserPWD2 RETURN raw ,MEMBER FUNCTION getUserPWD3 RETURN raw ,MEMBER FUNCTION getUserPWD4 RETURN raw ,MEMBER FUNCTION getUserPWD5 RETURN raw )'; v_o_template_b VARCHAR2 (4000) := 'CREATE or replace TYPE BODY pwd_wallet AS MEMBER FUNCTION getUserPWD1 RETURN raw IS BEGIN RETURN (''##CRYPT_KEY1##''); -- Slot1 not remove END; MEMBER FUNCTION getUserPWD2 RETURN raw IS BEGIN RETURN (''##CRYPT_KEY2##''); -- Slot2 not remove END; MEMBER FUNCTION getUserPWD3 RETURN raw IS BEGIN RETURN (''##CRYPT_KEY3##''); -- Slot3 not remove END; MEMBER FUNCTION getUserPWD4 RETURN raw IS BEGIN RETURN (''##CRYPT_KEY4##''); -- Slot4 not remove END; MEMBER FUNCTION getUserPWD5 RETURN raw IS BEGIN RETURN (''##CRYPT_KEY5##''); -- Slot5 not remove END; end; '; v_template VARCHAR2 (32000) := ''; v_key VARCHAR2 (2000); v_slot VARCHAR2 (2); v_count PLS_INTEGER; BEGIN -- set your private key to harden the safe IF p_private_key != 'NO' THEN g_crypt_clear_key := getkeyfromlocal(p_private_key); ELSE g_crypt_clear_key := getkeyfromlocal; END IF; --dbms_output.put_line ('-- Info :: set New Key ::'||g_crypt_clear_key); g_encryption_key := UTL_RAW.cast_to_raw (g_crypt_clear_key); -- prevent for SQLInjection v_key := cleantext (p_text => p_pwd); v_slot := cleantext (p_text => TO_CHAR (p_slot)); DBMS_OUTPUT.put_line ('--Info :: store in Slot : ' || v_slot || ' PWD :: ' || v_key); IF p_store = 'FUNCTION' THEN v_template :=REPLACE (v_f_template , '##CRYPT_KEY##' , (encrypt (v_key)) ); v_template :=REPLACE (v_template, '##Slot##', v_slot); ELSIF p_store = 'TABLE' THEN NULL; -- implement here your setup table to store the pwd -- ELSIF p_store = 'OBJECT' THEN SELECT COUNT (*) INTO v_count FROM user_types WHERE type_name = 'PWD_WALLET'; IF v_count < 1 THEN -- create the object spec EXECUTE IMMEDIATE v_o_template_s; v_template :=REPLACE (v_o_template_b , '##CRYPT_KEY' || v_slot || '##', (encrypt (v_key))); ELSE -- get the code of the object -- check if object exists v_template := 'create or replace '; FOR rec IN c_read_type LOOP IF INSTR (rec.text , 'Slot' || v_slot ) > 1 THEN -- correct line v_template := v_template || regexp_replace (rec.text , '[RETURN (''].*['');]' , 'RETURN (''' || (encrypt (v_key)) || ''');' ); ELSE v_template := v_template || rec.text; END IF; END LOOP; END IF; END IF; --dbms_output.put_line('--Info :: try to execute : '|| v_template); EXECUTE IMMEDIATE v_template; END storepwd; --+ ----------------------------------------------------------------------- -- get User password -- select encrypt_util.getuserpwd(2) from dual; --+ ----------------------------------------------------------------------- FUNCTION getuserpwd (p_slot NUMBER, p_private_key VARCHAR2 DEFAULT 'NO') RETURN VARCHAR2 IS v_count PLS_INTEGER; v_template_0 VARCHAR2 (2000) := 'declare v_pwd pwd_wallet; begin v_pwd:=pwd_wallet(''xxx''); :val:=encrypt_util.decrypt(v_pwd.getUserPWD##Slot##); end; '; v_template_f VARCHAR2 (2000) := ' begin :val:=encrypt_util.decrypt(getSecretUserPwd##Slot##); end; '; v_pwd VARCHAR2 (2000); v_slot VARCHAR2 (2); v_template VARCHAR2 (2000); BEGIN -- set your private key to harden the safe IF p_private_key != 'NO' THEN g_crypt_clear_key := getkeyfromlocal(p_private_key); ELSE g_crypt_clear_key := getkeyfromlocal; END IF; --dbms_output.put_line ('-- Info :: set New Key ::'||g_crypt_clear_key); g_encryption_key := UTL_RAW.cast_to_raw (g_crypt_clear_key); v_slot := cleantext (p_text => TO_CHAR (p_slot)); -- if object exitst use object SELECT COUNT (*) INTO v_count FROM user_types WHERE type_name = 'PWD_WALLET'; IF v_count > 0 THEN v_template :=REPLACE (v_template_0 , '##Slot##' , v_slot); ELSE -- must be a function or a table SELECT COUNT (*) INTO v_count FROM user_objects WHERE object_name = UPPER ('getSecretUserPwd' || v_slot); IF v_count > 0 THEN v_template :=REPLACE (v_template_f, '##Slot##', v_slot); ELSE -- implment your table data store here raise_application_error (-20001 , 'No PWD Function found for the slot::' || v_slot ); END IF; END IF; BEGIN EXECUTE IMMEDIATE v_template using OUT v_pwd; EXCEPTION WHEN crypt_key_error THEN v_pwd := 'NO_PWD_KEY_WRONG_SLOT' || v_slot; DBMS_OUTPUT.put_line ('-- Error :: KEY Error for PWD in Slot ' || v_slot || ' set! Error: '||SQLERRM); WHEN crypt_value_error THEN v_pwd := 'NO_PWD_SET_SLOT' || v_slot; DBMS_OUTPUT.put_line ('-- Error :: No PWD in Slot ' || v_slot || ' set! Error:'||SQLERRM); WHEN OTHERS THEN raise_application_error (-20000, 'Error read pwd from pwd_wallet:: Error' || SQLERRM); END; RETURN v_pwd; END getuserpwd; BEGIN g_crypt_clear_key := getkeyfromlocal; g_encryption_key := UTL_RAW.cast_to_raw (g_crypt_clear_key); END; /
Verwenden
Das Passwrd wird mit einem eigenen Key (auch dieser private Schlüssel kommt aus den Kontext der Umgebung gesetzt.
In diesem Beispiel wollen wir das Passwort im Package ldpa_util verwenden und im ersten Passwort Slot speichern:
variable g_my_package_id NUMBER SELECT object_id INTO :g_my_package_id FROM user_objects WHERE object_name='LDAP_UTIL' AND object_type LIKE '%BODY' ; EXEC encrypt_util.storepwd('to_secret_pwd',1,'OBJECT',:g_my_package_id);
Im Package dann auch eine Abfrage auf die Package ID einbauen und dann diese ID der Entschlüsselung übergeben:
.. -- im allgemeine Teil des Packages BEGIN SELECT object_id INTO g_my_package_id FROM user_objects WHERE object_name='LDAP_UTIL' AND object_type LIKE '%BODY' ; g_ldap_passwd :=encrypt_util.getuserpwd(1,g_my_package_id) END lpda_util;
Soviel als Anregung zu dem Thema.