Close

PBKDF2 in Oracle 12c

When I wrote my article on Oracle passwords I included a small, limited implementation of the PBKDF2 algorithm in order to illustrate the 12c hashing methodology.    Here I’m publishing a fuller implementation with parameters for determining the derived key length, number of hashing iterations, and a choice of hashing methods (SH1, SH256, SH384, or SH512.)  I’ve included methods to return a RAW value (up to 32K octets/bytes) and VARCHAR2 (up to 16K octets, 32K hex characters) as well as BLOB and CLOB variations if you happen to need a large hash value.

The VARCHAR2 version is simply RAWTOHEX wrapped around the get_raw.  The BLOB and CLOB follow the same overall logic but have some small internal optimizations to help with extremely large results and/or numerous iterations.

My implementation is a fairly straight forward adaption of documentation in section 5.2 of RFC2898.   The only significant deviation is with the BLOB and CLOB functions where I batch up intermediate concatenations within a RAW or VARCHAR2 before concatenating to the BLOB or CLOB result.  I do this for efficiency because lob operations are slower.

First, a couple of use cases to test the functionality.  These were both taken from the test vectors published in RFC6070.

SQL> select pbkdf2.get_hex('password',utl_raw.cast_to_raw('salt'),1,20,2) from dual;

PBKDF2.GET_HEX('PASSWORD',UTL_RAW.CAST_TO_RAW('SALT'),1,20,2)
-------------------------------------------------------------------------------------------
0C60C80F961F0E71F3A9B524AF6012062FE037A6

SQL> select pbkdf2.get_hex('password',utl_raw.cast_to_raw('salt'),4096,20,2) from dual;

PBKDF2.GET_HEX('PASSWORD',UTL_RAW.CAST_TO_RAW('SALT'),4096,20,2)
-------------------------------------------------------------------------------------------
4B007901B765489ABEAD49D926F721D065A429C1

And now, the PBKDF2 package

CREATE OR REPLACE PACKAGE pbkdf2
IS
    --                    .///.
    --                   (0 o)
    ---------------0000--(_)--0000---------------
    --
    --  Sean D. Stuber
    --  sean.stuber@gmail.com
    --
    --             oooO      Oooo
    --------------(   )-----(   )---------------
    --             \ (       ) /
    --              \_)     (_/

    -- Implementation of algorithm described in section 5.2 of RFC2898
    -- https://tools.ietf.org/html/rfc2898

    -- dk_length refers to number of octets returned for the desired key
    -- regardless of whether the result is raw/blob or hex characters in varchar2/clob
    --   So, a 20-octet key returned by get_raw, would be a 40 character hex string
    --   returned by get_hex.  The dk_length parameter would be 20 in both cases.
    
    -- The following HMAC algorithms are supported
    --   DBMS_CRYPTO.HMAC_SH1    = 2
    --   DBMS_CRYPTO.HMAC_SH256  = 3 
    --   DBMS_CRYPTO.HMAC_SH384  = 4
    --   DBMS_CRYPTO.HMAC_SH512  = 5

    -- Test vectors
    --   https://tools.ietf.org/html/rfc6070

    --  select pbkdf2.get_hex('password',utl_raw.cast_to_raw('salt'),1,20,2) from dual;
    --      0C60C80F961F0E71F3A9B524AF6012062FE037A6 
    --  select pbkdf2.get_hex('password',utl_raw.cast_to_raw('salt'),2,20,2) from dual;
    --      EA6C014DC72D6F8CCD1ED92ACE1D41F0D8DE8957
    --  select pbkdf2.get_hex('password',utl_raw.cast_to_raw('salt'),4096,20,2) from dual;
    --      4B007901B765489ABEAD49D926F721D065A429C1
    --  select pbkdf2.get_hex('passwordPASSWORDpassword',utl_raw.cast_to_raw('saltSALTsaltSALTsaltSALTsaltSALTsalt'),4096,25,2) from dual;
    --      3D2EEC4FE41C849B80C8D83662C0E44A8B291A964CF2F07038

    FUNCTION get_raw(
        p_password     IN VARCHAR2,
        p_salt         IN RAW,
        p_iterations   IN PLS_INTEGER,
        p_dk_length    IN PLS_INTEGER,
        p_hmac         IN PLS_INTEGER DEFAULT DBMS_CRYPTO.hmac_sh512
    )
        RETURN RAW
        DETERMINISTIC;

    FUNCTION get_hex(
        p_password     IN VARCHAR2,
        p_salt         IN RAW,
        p_iterations   IN PLS_INTEGER,
        p_dk_length    IN PLS_INTEGER,
        p_hmac         IN PLS_INTEGER DEFAULT DBMS_CRYPTO.hmac_sh512
    )
        RETURN VARCHAR2
        DETERMINISTIC;

    FUNCTION get_blob(
        p_password     IN VARCHAR2,
        p_salt         IN RAW,
        p_iterations   IN PLS_INTEGER,
        p_dk_length    IN PLS_INTEGER,
        p_hmac         IN PLS_INTEGER DEFAULT DBMS_CRYPTO.hmac_sh512
    )
        RETURN BLOB
        DETERMINISTIC;

    FUNCTION get_clob(
        p_password     IN VARCHAR2,
        p_salt         IN RAW,
        p_iterations   IN PLS_INTEGER,
        p_dk_length    IN PLS_INTEGER,
        p_hmac         IN PLS_INTEGER DEFAULT DBMS_CRYPTO.hmac_sh512
    )
        RETURN CLOB
        DETERMINISTIC;
END;
/
CREATE OR REPLACE PACKAGE BODY pbkdf2
IS
    --                    .///.
    --                   (0 o)
    ---------------0000--(_)--0000---------------
    --
    --  Sean D. Stuber
    --  sean.stuber@gmail.com
    --
    --             oooO      Oooo
    --------------(   )-----(   )---------------
    --             \ (       ) /
    --              \_)     (_/

    -- Implementation of algorithm described in section 5.2 of RFC2898
    -- https://tools.ietf.org/html/rfc2898

    -- dk_length refers to number of octets returned for the desired key
    -- regardless of whether the result is raw/blob or hex characters in varchar2/clob
    --   So, a 20-octet key returned by get_raw, would be a 40 character hex string
    --   returned by get_hex.  The dk_length parameter would be 20 in both cases.

    -- The following HMAC algorithms are supported
    --   DBMS_CRYPTO.HMAC_SH1    = 2
    --   DBMS_CRYPTO.HMAC_SH256  = 3
    --   DBMS_CRYPTO.HMAC_SH384  = 4
    --   DBMS_CRYPTO.HMAC_SH512  = 5

    c_max_raw_length   CONSTANT PLS_INTEGER := 32767;
    c_max_hex_length   CONSTANT PLS_INTEGER := 32767;

    SUBTYPE t_maxraw IS RAW(32767);

    SUBTYPE t_maxhex IS VARCHAR2(32767);

    SUBTYPE t_hmac_result IS RAW(64);           -- must be big enough to hold largest supported HMAC

    FUNCTION iterate_hmac_xor(
        p_salt             IN RAW,
        p_iterations       IN PLS_INTEGER,
        p_hmac             IN PLS_INTEGER,
        p_block_iterator   IN PLS_INTEGER,
        p_raw_password     IN RAW
    )
        RETURN t_hmac_result
    IS
        v_u           t_maxraw;
        v_f_xor_sum   t_hmac_result;
    BEGIN
        -- The RFC describes the U(1)...U(c) values recursively
        -- but the implementation below simply loops with a stored value
        -- to achieve the same functionality.
        v_u :=
            UTL_RAW.CONCAT(
                p_salt,
                UTL_RAW.cast_from_binary_integer(p_block_iterator, UTL_RAW.big_endian)
            );

        v_u := DBMS_CRYPTO.mac(src => v_u, typ => p_hmac, key => p_raw_password);
        v_f_xor_sum := v_u;

        FOR c IN 2 .. p_iterations
        LOOP
            v_u := DBMS_CRYPTO.mac(src => v_u, typ => p_hmac, key => p_raw_password);
            v_f_xor_sum := UTL_RAW.bit_xor(v_f_xor_sum, v_u);
        END LOOP;

        RETURN v_f_xor_sum;
    END iterate_hmac_xor;

    FUNCTION get_raw(
        p_password     IN VARCHAR2,
        p_salt         IN RAW,
        p_iterations   IN PLS_INTEGER,
        p_dk_length    IN PLS_INTEGER,
        p_hmac         IN PLS_INTEGER DEFAULT DBMS_CRYPTO.hmac_sh512
    )
        RETURN RAW
        DETERMINISTIC
    IS
        c_hlen           CONSTANT PLS_INTEGER
            := CASE p_hmac
                   WHEN DBMS_CRYPTO.hmac_sh1 THEN 20
                   WHEN DBMS_CRYPTO.hmac_sh256 THEN 32
                   WHEN DBMS_CRYPTO.hmac_sh384 THEN 48
                   WHEN DBMS_CRYPTO.hmac_sh512 THEN 64
               END ;

        c_octet_blocks   CONSTANT PLS_INTEGER := CEIL(p_dk_length / c_hlen);
        v_t_concat                t_maxraw := NULL;
        v_block_iterator          PLS_INTEGER := 1;
    BEGIN
        -- raise exception message per rfc
        -- but this limit is rather moot since the function
        -- is capped by raw limits
        IF p_dk_length > (POWER(2, 32) - 1) * c_hlen
        THEN
            raise_application_error(-20001, 'derived key too long');
        ELSIF p_dk_length > c_max_raw_length
        THEN
            raise_application_error(-20001, 'raw output must be less than to 32K bytes');
        END IF;

        IF p_iterations < 1
        THEN
            raise_application_error(-20001, 'must iterate at least once');
        END IF;

        -- Loop one block of hlen-octets at a time of the derived key.
        -- If we build a key past the desired length then exit early, no need to continue
        WHILE     v_block_iterator <= c_octet_blocks
              AND (v_t_concat IS NULL OR UTL_RAW.LENGTH(v_t_concat) < p_dk_length)
        LOOP
            v_t_concat :=
                UTL_RAW.CONCAT(
                    v_t_concat,
                    iterate_hmac_xor(
                        p_salt,
                        p_iterations,
                        p_hmac,
                        v_block_iterator,
                        UTL_RAW.cast_to_raw(p_password)
                    )
                );
            v_block_iterator := v_block_iterator + 1;
        END LOOP;

        RETURN UTL_RAW.SUBSTR(v_t_concat, 1, p_dk_length);
    END get_raw;

    FUNCTION get_hex(
        p_password     IN VARCHAR2,
        p_salt         IN RAW,
        p_iterations   IN PLS_INTEGER,
        p_dk_length    IN PLS_INTEGER,
        p_hmac         IN PLS_INTEGER DEFAULT DBMS_CRYPTO.hmac_sh512
    )
        RETURN VARCHAR2
        DETERMINISTIC
    IS
    BEGIN
        -- raise exception message per rfc
        -- but this limit is rather moot since the function
        -- is capped by varchar2 limits
        IF p_dk_length > c_max_raw_length / 2
        THEN
            raise_application_error(-20001, 'hex representation must be less than 32K characters');
        END IF;

        RETURN RAWTOHEX(
                   get_raw(
                       p_password,
                       p_salt,
                       p_iterations,
                       p_dk_length,
                       p_hmac
                   )
               );
    END get_hex;

    FUNCTION get_blob(
        p_password     IN VARCHAR2,
        p_salt         IN RAW,
        p_iterations   IN PLS_INTEGER,
        p_dk_length    IN PLS_INTEGER,
        p_hmac         IN PLS_INTEGER DEFAULT DBMS_CRYPTO.hmac_sh512
    )
        RETURN BLOB
        DETERMINISTIC
    IS
        c_hlen           CONSTANT PLS_INTEGER
            := CASE p_hmac
                   WHEN DBMS_CRYPTO.hmac_sh1 THEN 20
                   WHEN DBMS_CRYPTO.hmac_sh256 THEN 32
                   WHEN DBMS_CRYPTO.hmac_sh384 THEN 48
                   WHEN DBMS_CRYPTO.hmac_sh512 THEN 64
               END ;

        c_octet_blocks   CONSTANT PLS_INTEGER := CEIL(p_dk_length / c_hlen);
        v_t_concat                BLOB;
        v_block_iterator          PLS_INTEGER := 1;
        v_temp                    t_maxraw;
    BEGIN
        -- raise exception message per rfc
        IF p_dk_length > (POWER(2, 32) - 1) * c_hlen
        THEN
            raise_application_error(-20001, 'derived key too long');
        END IF;

        IF p_iterations < 1
        THEN
            raise_application_error(-20001, 'must iterate at least once');
        END IF;

        DBMS_LOB.createtemporary(lob_loc => v_t_concat, cache => FALSE, dur => DBMS_LOB.session);

        -- Loop one block of hlen-octets at a time of the derived key.
        -- If we build a key past the desired length then exit early, no need to continue
        WHILE v_block_iterator <= c_octet_blocks AND (DBMS_LOB.getlength(v_t_concat) < p_dk_length)
        LOOP
            v_temp :=
                UTL_RAW.CONCAT(
                    v_temp,
                    RAWTOHEX(
                        iterate_hmac_xor(
                            p_salt,
                            p_iterations,
                            p_hmac,
                            v_block_iterator,
                            UTL_RAW.cast_to_raw(p_password)
                        )
                    )
                );

            -- Concatenating raw is faster than blob
            -- only concatenate to the blob when the temp raw fills up
            IF UTL_RAW.LENGTH(v_temp) > c_max_raw_length - c_hlen
            THEN
                DBMS_LOB.writeappend(v_t_concat, UTL_RAW.LENGTH(v_temp), v_temp);
                v_temp := NULL;
            END IF;

            v_block_iterator := v_block_iterator + 1;
        END LOOP;

        DBMS_LOB.writeappend(v_t_concat, UTL_RAW.LENGTH(v_temp), v_temp);

        DBMS_LOB.TRIM(v_t_concat, p_dk_length);

        RETURN v_t_concat;
    END get_blob;

    FUNCTION get_clob(
        p_password     IN VARCHAR2,
        p_salt         IN RAW,
        p_iterations   IN PLS_INTEGER,
        p_dk_length    IN PLS_INTEGER,
        p_hmac         IN PLS_INTEGER DEFAULT DBMS_CRYPTO.hmac_sh512
    )
        RETURN CLOB
        DETERMINISTIC
    IS
        c_hlen           CONSTANT PLS_INTEGER
            := CASE p_hmac
                   WHEN DBMS_CRYPTO.hmac_sh1 THEN 20
                   WHEN DBMS_CRYPTO.hmac_sh256 THEN 32
                   WHEN DBMS_CRYPTO.hmac_sh384 THEN 48
                   WHEN DBMS_CRYPTO.hmac_sh512 THEN 64
               END ;

        c_octet_blocks   CONSTANT PLS_INTEGER := CEIL(p_dk_length / c_hlen);
        v_t_concat                CLOB;
        v_block_iterator          PLS_INTEGER := 1;
        v_temp                    t_maxhex;
    BEGIN
        -- raise exception message per rfc
        IF p_dk_length > (POWER(2, 32) - 1) * c_hlen
        THEN
            raise_application_error(-20001, 'derived key too long');
        END IF;

        IF p_iterations < 1
        THEN
            raise_application_error(-20001, 'must iterate at least once');
        END IF;

        DBMS_LOB.createtemporary(lob_loc => v_t_concat, cache => FALSE, dur => DBMS_LOB.session);

        -- Loop one block of hlen-octets at a time of the derived key.
        -- If we build a key past the desired length then exit early, no need to continue
        -- The end result is a HEX string, so double the length (2 characters to represent one byte)
        WHILE     v_block_iterator <= c_octet_blocks
              AND (DBMS_LOB.getlength(v_t_concat) < p_dk_length * 2)
        LOOP
            v_temp :=
                   v_temp
                || RAWTOHEX(
                       iterate_hmac_xor(
                           p_salt,
                           p_iterations,
                           p_hmac,
                           v_block_iterator,
                           UTL_RAW.cast_to_raw(p_password)
                       )
                   );

            -- Concatenating varchar2 is faster than clob
            -- only concatenate to the clob when the temp varchar2 fills up
            IF LENGTH(v_temp) > c_max_hex_length - 2 * c_hlen
            THEN
                v_t_concat := v_t_concat || v_temp;
                v_temp := NULL;
            END IF;

            v_block_iterator := v_block_iterator + 1;
        END LOOP;

        v_t_concat := v_t_concat || v_temp;
        DBMS_LOB.TRIM(v_t_concat, p_dk_length * 2);

        RETURN v_t_concat;
    END get_clob;
END;
/

The PBKDF2 algorithm is supposed to be slow to execute in order to discourage brute force hacking attempts. While I did use a few coding techniques to try to help performance, this is still not a fast implementation. If you need maximal speed then I recommend a c library. For short strings and less than 100000 iterations the package should have adequate speed for most use cases.

The code presented above requires at least 12cR1, but could run on lower versions by changing the supported hashing methods.

I hope it helps, questions and comments are always welcome.

2 thoughts on “PBKDF2 in Oracle 12c

  1. Hi, thanks a lot for sharing your implementation.
    Unfortunatly it doesn’t compile. The get_raw method has an error at:
    WHILE v_block_iterator <= c_octet_blocks
    AND (v_t_concat IS NULL OR UTL_RAW.LENGTH(v_t_concat)
    c_max_raw_length / 2
    THEN
    raise_application_error(-20001, 'hex representation must be less than 32K characters');
    END IF;

    For sure a problem of wordpress 🙂

    1. Thank you for reading and bringing the problem to my attention.
      Your assessment is correct – wordpress was “formatting” my code. I’ve corrected it and the embedded code blocks should be readable and compilable now.

      Thanks again!

Comments are closed.