Call: +44 (0)7759 277220 Call
PeteFinnigan.com Limited Products, Services, Training and Information
Blog

Pete Finnigan's Oracle Security Weblog

This is the weblog for Pete Finnigan. Pete works in the area of Oracle security and he specialises in auditing Oracle databases for security issues. This weblog is aimed squarely at those interested in the security of their Oracle databases.

Cluster Objects in the Oracle Database

Sometimes in forensic analysis of an Oracle database it is necessary to understand how data is stored on disk. Actually we must understand that if we were to create our own database we might start with a file that holds the data, maybe storing the data just as text lines or as JSON or XML or whatever. Eventually for speed we might place the data in fixed blocks and to allow query of that data we might also store a description of that data storage also in the same datafile.

The Oracle database is incredibly complex and most likely millions of lines of C code but at the deepest depths its just a set of files on disk that holds our data. The Oracle software allows us to query and retrieve or update our data from those files. BUT, at the simplest level the data is just stored in files with incredibly complex software to retrieve and work with that data.

At a high level we store data in tables and maybe we use indexes to make finding certain records faster. These structures are built into meta data that allows access to the right blocks of data. In general one object is stored in each data block but sometimes Oracle wants to access two objects in a block to make the access more efficient. This is achieved with a cluster. First a cluster is created and then multiple tables can be added to the clustered storage all indexed by a key; an indexed column. This is not an index the Oracle traditional sense; it is a column that all rows of each table type possess so that the rows from different tables can be accessed faster / together

The SYS.USER$ table is in a cluster. The cluster is created in dcore.bsq which you can find on the database server:

...
create cluster c_user#(user# number)
size 372 /* cluster key ~ 20, sizeof(user$) ~ 227, 5 * sizeof(tsq$) ~ 125 */
/
create index i_user# on cluster c_user#
/
...

create table user$ /* user table */
( user# number not null, /* user identifier number */
name varchar2("M_IDEN") not null, /* name of user */
...
)
cluster c_user#(user#)
/

create table tsq$ /* tablespace quota table */
( ts# number not null, /* tablespace number */
user# number not null, /* user number */
grantor# number not null, /* grantor id */
blocks number not null, /* number of blocks charged to user */
maxblocks number, /* user's maximum number of blocks, NULL if none */
priv1 number not null, /* reserved for future privilege */
priv2 number not null, /* reserved for future privilege */
priv3 number not null) /* reserved for future privilege */
cluster c_user# (user#)
/

There are 3 objects in this c_user# cluster; the i_user# index, user$ table itself and the tsq$ the tablespace quotas table.

We can check what clusters exist:

SQL> set serveroutput on
SQL> @sc_print 'select * from dba_clusters'
old 32: lv_str:=translate('&&1','''','''''');
new 32: lv_str:=translate('select * from dba_clusters','''','''''');
Executing Query [select * from dba_clusters]
...
-------------------------------------------
OWNER : SYS
CLUSTER_NAME : C_USER#
TABLESPACE_NAME : SYSTEM
PCT_FREE : 10
PCT_USED : 40
KEY_SIZE : 372
INI_TRANS : 2
MAX_TRANS : 255
INITIAL_EXTENT : 65536
NEXT_EXTENT : 1048576
MIN_EXTENTS : 1
MAX_EXTENTS : 2147483645
PCT_INCREASE :
FREELISTS : 1
FREELIST_GROUPS : 1
AVG_BLOCKS_PER_KEY :
CLUSTER_TYPE : INDEX
FUNCTION :
HASHKEYS : 0
DEGREE : 1
INSTANCES : 1
CACHE : N
BUFFER_POOL : DEFAULT
FLASH_CACHE : DEFAULT
CELL_FLASH_CACHE : DEFAULT
SINGLE_TABLE : N
DEPENDENCIES : DISABLED
...

As we can see the c_user# cluster is an INDEX type. There are two types of cluster, an INDEX type which uses a B tree index or a HASH which uses a hash function which also must be provided. What is the purpose of a cluster? in a normal case a data block holds one object such as table. If there are regular joins between two tables such as USER$.USER# and TSQ$.USER# then creating a cluster allows rows from both tables to be stored in the same data block. This allows faster access to the linked data as only one block visit is required for both tables indexed on the USER# column.

We are interested in this as during forensics investigations we often focus on dictionary tables and in particular the bootstrap tables that are loaded from fixed positions. Oracle has to bootstrap itself into a running instance and most SQL involved recursive SQL that queries user$, obj$ and more. These tables have to be loaded first to allow any more SQL to run as it uses these tables first in recursive SQL. As these tables such as USER$ and OBJ$ are accessed in almost all SQL statements in the background then it makes sense to cluster them.

Can we create or own clusters?

SQL> connect c##acco/oracle1@//192.168.56.33:1539/xepdb1
Connected.
SQL> create user cluster_user identified by cluster_user;

User created.

SQL> grant create session to cluster_user;

Grant succeeded.

SQL> connect sys/oracle1@//192.168.56.33:1539/xepdb1 as sysdba
Connected.
SQL> grant create table to cluster_user;

Grant succeeded.

SQL> grant create cluster to cluster_user;

Grant succeeded.

SQL> grant unlimited tablespace to cluster_user;

Grant succeeded.

SQL>

NOTE: I have Database Vault enabled in this database hence the need to use c##acco to create a user

Now create a sample cluster:

SQL> connect cluster_user/cluster_user@//192.168.56.33:1539/xepdb1
Connected.
SQL> create cluster c_pete(id number) size 512;

Cluster created.

SQL> create table test_1 (id number, name varchar2(100)) cluster c_pete(id);

Table created.

SQL> create table test_2 (id number, address varchar2(100)) cluster c_pete(id);

Table created.

SQL>

Now check DBA_CLSUERS to see the entry:

SQL> connect sys/oracle1@//192.168.56.33:1539/xepdb1 as sysdba
Connected.
SQL> set serveroutput on
SQL> @sc_print 'select * from dba_clusters where cluster_name=''''C_PETE'''''
old 32: lv_str:=translate('&&1','''','''''');
new 32: lv_str:=translate('select * from dba_clusters where cluster_name=''C_PETE''','''','''''');
Executing Query [select * from dba_clusters where cluster_name='C_PETE']
OWNER : CLUSTER_USER
CLUSTER_NAME : C_PETE
TABLESPACE_NAME : USERS
PCT_FREE : 10
PCT_USED :
KEY_SIZE : 512
INI_TRANS : 2
MAX_TRANS : 255
INITIAL_EXTENT : 65536
NEXT_EXTENT : 1048576
MIN_EXTENTS : 1
MAX_EXTENTS : 2147483645
PCT_INCREASE :
FREELISTS :
FREELIST_GROUPS :
AVG_BLOCKS_PER_KEY :
CLUSTER_TYPE : INDEX
FUNCTION :
HASHKEYS : 0
DEGREE : 1
INSTANCES : 1
CACHE : N
BUFFER_POOL : DEFAULT
FLASH_CACHE : DEFAULT
CELL_FLASH_CACHE : DEFAULT
SINGLE_TABLE : N
DEPENDENCIES : DISABLED
-------------------------------------------

PL/SQL procedure successfully completed.

SQL>

We can see that the cluster is INDEX type and the index size is 512 as specified.

A table is still a table BUT we can group tables together in a cluster so that they are stored in the same data block for speed

What about the key we specified in the creation of the cluster and also used when we placed tables in a cluster?

SQL> col cluster_name for a10
SQL> col clu_column_name for a10
SQL> col table_name for a10
SQL> col tab_column_name for a10
SQL> set lines 220
SQL> select cluster_name,clu_column_name,table_name,tab_column_name from dba_clu_columns where owner='CLUSTER_USER';

CLUSTER_NA CLU_COLUMN TABLE_NAME TAB_COLUMN
---------- ---------- ---------- ----------
C_PETE ID TEST_2 ID
C_PETE ID TEST_1 ID

SQL>

This show the index column per table that are in the cluster.

The cluster key is not visible in DBA_CLUSTERING_KEYS or DBA_CLUSTERS, so how can we find the name of the key independent of the table definition?

We can use DBMS_METADATA.GET_DDL to get the cluster index column without respect to tables in the cluser:

SQL> SELECT DBMS_METADATA.GET_DDL('CLUSTER','C_PETE','CLUSTER_USER') from dual;

DBMS_METADATA.GET_DDL('CLUSTER','C_PETE','CLUSTER_USER')
--------------------------------------------------------------------------------

CREATE CLUSTER "CLUSTER_USER"."C_PETE" (
"ID" NUMBER )
SIZE 512
PCTFREE 10 PCTUSED 40 INITRANS 2 MAXTRANS 255
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1
BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
TABLESPACE "USERS"
PARALLEL (DEGREE 1 INSTANCES 1)


SQL>

Because the DDL can be re-constructed the index column of the cluster has to be stored somewhere in the data dictionary. The cluster index is not exposed in DBA_CLUSTERS and the DBA_CLU_COLUMNS so we have to get it from the base tables:

SQL> col owner# for 9999
SQL> col cluster_name for a10
SQL> col col# for 99
SQL> col cluster_key_column for a10
SQL> col type# for 999
SQL> SELECT o.owner#,
2 o.name AS cluster_name,
3 c.col#,
4 c.name AS cluster_key_column,
5 c.type#
6 FROM sys.obj$ o
7 JOIN sys.col$ c
8 ON o.obj# = c.obj#
9 WHERE o.type# = 3 -- cluster object
10 AND o.name = 'C_PETE';

OWNER# CLUSTER_NA COL# CLUSTER_KE TYPE#
------ ---------- ---- ---------- -----
513 C_PETE 1 ID 2

SQL>

Understanding the internals of how some data is stored and in this case for clustered objects is important for forensics analysis of an Oracle database that may have been breached or in general to understand how features work in the database

#oracleace #sym_42 #oracle #forensics #database #security #internals #clusters

AI Comparison for Oracle Security Code Generation

I am running a Mac Book Pro M5 with 64gb of RAM and a 2tb SSD. I wanted to use local LLMs to generate Oracle security related PL/SQL to see how good these models are at generating PL/SQL code and also how they perform against each other in terms of elapsed time and also tokens per second. In AI we can measure the speed of the AI in elapsed time and also tokens per second. Tokens are the chunks of text that the LLM then processes. They can be words or parts of words.

I tested three models, qwen2.5-coder, qwen3-coder:30b and qwen3-coder-next.

I used the same prompt for each of the models. The prompt is:
please write a PL/SQL public package procedures and any supporting private procedures and functions where that public procedure can encrypt and decrypt data passed and base64 encode/decode the result

I ran qwen2.5-coder and it generated this text at the start:

Certainly! Below is a complete PL/SQL package named encrypt_decrypt_pkg that includes public procedures for encrypting and decrypting data, as well as base64 encoding and decoding. The package uses AES256 encryption with CBC mode and PKCS#5 padding.

and it went on to generate a PL/SQL package header, package body and some example use cases of the resultant code. The header generated is:

CREATE OR REPLACE PACKAGE encrypt_decrypt_pkg AS
-- Public procedure to encrypt data and base64 encode it
PROCEDURE encrypt_and_encode(
p_data IN VARCHAR2,
p_encrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
);

-- Public procedure to decrypt base64 decoded data
PROCEDURE decode_and_decrypt(
p_encrypted_data IN VARCHAR2,
p_decrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
);

-- Public procedure to base64 encode data
PROCEDURE base64_encode(
p_data IN VARCHAR2,
p_encoded_data OUT VARCHAR2
);

-- Public procedure to base64 decode data
PROCEDURE base64_decode(
p_encoded_data IN VARCHAR2,
p_decoded_data OUT VARCHAR2
);
END encrypt_decrypt_pkg;
/

The package body generated is:

CREATE OR REPLACE PACKAGE BODY encrypt_decrypt_pkg AS
-- Private function to generate a key for encryption
FUNCTION generate_key(p_key IN VARCHAR2 DEFAULT NULL) RETURN RAW IS
v_key RAW(32);
BEGIN
IF p_key IS NOT NULL THEN
-- Use provided key and hash it to 256 bits
v_key := DBMS_CRYPTO.HASH(UTL_I18N.STRING_TO_RAW(p_key, 'AL32UTF8'), DBMS_CRYPTO.HASH_SH256);
ELSE
-- Generate a random key
v_key := DBMS_CRYPTO.RANDOMBYTES(32);
END IF;

RETURN v_key;
END generate_key;

-- Private function to base64 encode raw data
FUNCTION base64_encode_raw(p_data IN RAW) RETURN VARCHAR2 IS
v_encoded RAW(32767);
BEGIN
v_encoded := UTL_ENCODE.BASE64_ENCODE(p_data, TRUE);
RETURN UTL_RAW.CAST_TO_VARCHAR2(v_encoded);
END base64_encode_raw;

-- Private function to base64 decode string to raw data
FUNCTION base64_decode_raw(p_data IN VARCHAR2) RETURN RAW IS
v_decoded RAW(32767);
BEGIN
v_decoded := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(p_data));
RETURN v_decoded;
END base64_decode_raw;

-- Public procedure to encrypt data and base64 encode it
PROCEDURE encrypt_and_encode(
p_data IN VARCHAR2,
p_encrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
) IS
v_key RAW(32);
v_iv RAW(16); -- Initialization Vector for AES-256-CBC
v_encrypted_raw RAW(32767);
BEGIN
-- Generate encryption key
v_key := generate_key(p_key);

-- Generate a random initialization vector (IV)
v_iv := DBMS_CRYPTO.RANDOMBYTES(16);

-- Encrypt data using AES256 in CBC mode with PKCS#5 padding
v_encrypted_raw := DBMS_CRYPTO.ENCRYPT(
src => UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8'),
typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC_PKCS5,
key => v_key,
iv => v_iv
);

-- Prepend the IV to the encrypted data and base64 encode it
p_encrypted_data := base64_encode_raw(v_iv || v_encrypted_raw);
END encrypt_and_encode;

-- Public procedure to decrypt base64 decoded data
PROCEDURE decode_and_decrypt(
p_encrypted_data IN VARCHAR2,
p_decrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
) IS
v_key RAW(32);
v_iv RAW(16); -- Initialization Vector for AES-256-CBC
v_combined_raw RAW(32767);
v_decrypted_raw RAW(32767);
BEGIN
-- Generate decryption key
v_key := generate_key(p_key);

-- Base64 decode the input data
v_combined_raw := base64_decode_raw(p_encrypted_data);

-- Extract the IV from the beginning of the combined data
v_iv := UTL_RAW.SUBSTR(v_combined_raw, 1, 16);

-- Extract the encrypted data after the IV
v_decrypted_raw := UTL_RAW.SUBSTR(v_combined_raw, 17);

-- Decrypt the data using AES256 in CBC mode with PKCS#5 padding
p_decrypted_data := UTL_I18N.RAW_TO_STRING(
DBMS_CRYPTO.DECRYPT(
src => v_decrypted_raw,
typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC_PKCS5,
key => v_key,
iv => v_iv
),
'AL32UTF8'
);
END decode_and_decrypt;

-- Public procedure to base64 encode data
PROCEDURE base64_encode(
p_data IN VARCHAR2,
p_encoded_data OUT VARCHAR2
) IS
v_raw_data RAW(32767);
BEGIN
-- Convert string to raw data
v_raw_data := UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8');

-- Base64 encode the raw data
p_encoded_data := base64_encode_raw(v_raw_data);
END base64_encode;

-- Public procedure to base64 decode data
PROCEDURE base64_decode(
p_encoded_data IN VARCHAR2,
p_decoded_data OUT VARCHAR2
) IS
v_decoded_raw RAW(32767);
BEGIN
-- Base64 decode the input data
v_decoded_raw := base64_decode_raw(p_encoded_data);

-- Convert raw data back to string
p_decoded_data := UTL_I18N.RAW_TO_STRING(v_decoded_raw, 'AL32UTF8');
END base64_decode;
END encrypt_decrypt_pkg;
/

The example usage code is:

-- Example 1: Encrypt and encode data
DECLARE
v_encrypted VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.encrypt_and_encode(
p_data => 'Hello World',
p_encrypted_data => v_encrypted,
p_key => 'my_secret_key'
);
DBMS_OUTPUT.PUT_LINE('Encrypted and encoded: ' || v_encrypted);
END;
/

-- Example 2: Decode and decrypt data
DECLARE
v_decrypted VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.decode_and_decrypt(
p_encrypted_data => 'your_encrypted_string_here',
p_decrypted_data => v_decrypted,
p_key => 'my_secret_key'
);
DBMS_OUTPUT.PUT_LINE('Decrypted data: ' || v_decrypted);
END;
/

-- Example 3: Simple base64 encoding
DECLARE
v_encoded VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.base64_encode(
p_data => 'Hello World',
p_encoded_data => v_encoded
);
DBMS_OUTPUT.PUT_LINE('Base64 encoded: ' || v_encoded);
END;
/

-- Example 4: Simple base64 decoding
DECLARE
v_decoded VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.base64_decode(
p_encoded_data => 'SGVsbG8gV29ybGQ=',
p_decoded_data => v_decoded
);
DBMS_OUTPUT.PUT_LINE('Base64 decoded: ' || v_decoded);
END;
/

No privileges are generated for DBMS_CRYPTO or UTL_ENCODE so we will do these manually at the end but first what did qwen3-coder:30b generate?

Here's a complete PL/SQL package for encryption/decryption with Base64 encoding/decoding. It generated the package header and body together unlike qwen2.5-coder:

CREATE OR REPLACE PACKAGE encrypt_decrypt_pkg AS
-- Public procedure to encrypt data and base64 encode it
PROCEDURE encrypt_and_encode(
p_data IN VARCHAR2,
p_encrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
);

-- Public procedure to decrypt base64 decoded data
PROCEDURE decode_and_decrypt(
p_encrypted_data IN VARCHAR2,
p_decrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
);

-- Public procedure to base64 encode data
PROCEDURE base64_encode(
p_data IN VARCHAR2,
p_encoded_data OUT VARCHAR2
);

-- Public procedure to base64 decode data
PROCEDURE base64_decode(
p_encoded_data IN VARCHAR2,
p_decoded_data OUT VARCHAR2
);
END encrypt_decrypt_pkg;
/

CREATE OR REPLACE PACKAGE BODY encrypt_decrypt_pkg AS
-- Private function to generate a key for encryption
FUNCTION generate_key(p_key IN VARCHAR2 DEFAULT NULL) RETURN RAW IS
v_key RAW(32);
BEGIN
IF p_key IS NOT NULL THEN
-- Use provided key
v_key := DBMS_CRYPTO.HASH(p_key, DBMS_CRYPTO.HASH_SH256);
ELSE
-- Generate a random key
v_key := DBMS_CRYPTO.RANDOMBYTES(32);
END IF;

RETURN v_key;
END generate_key;

-- Private function to base64 encode raw data
FUNCTION base64_encode_raw(p_data IN RAW) RETURN VARCHAR2 IS
v_encoded VARCHAR2(32767);
BEGIN
v_encoded := UTL_ENCODE.BASE64_ENCODE(p_data);
RETURN v_encoded;
END base64_encode_raw;

-- Private function to base64 decode string to raw data
FUNCTION base64_decode_raw(p_data IN VARCHAR2) RETURN RAW IS
v_decoded RAW(32767);
BEGIN
v_decoded := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(p_data));
RETURN v_decoded;
END base64_decode_raw;

-- Public procedure to encrypt data and base64 encode it
PROCEDURE encrypt_and_encode(
p_data IN VARCHAR2,
p_encrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
) IS
v_key RAW(32);
v_encrypted_raw RAW(32767);
BEGIN
-- Generate encryption key
v_key := generate_key(p_key);

-- Encrypt data using AES256 in CBC mode with PKCS#5 padding
v_encrypted_raw := DBMS_CRYPTO.ENCRYPT(
src => UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8'),
typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC + DBMS_CRYPTO.PKCS5,
key => v_key
);

-- Base64 encode the encrypted data
p_encrypted_data := base64_encode_raw(v_encrypted_raw);
END encrypt_and_encode;

-- Public procedure to decrypt base64 decoded data
PROCEDURE decode_and_decrypt(
p_encrypted_data IN VARCHAR2,
p_decrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
) IS
v_key RAW(32);
v_decrypted_raw RAW(32767);
BEGIN
-- Generate decryption key
v_key := generate_key(p_key);

-- Base64 decode the input data
v_decrypted_raw := base64_decode_raw(p_encrypted_data);

-- Decrypt the data using AES256 in CBC mode with PKCS#5 padding
p_decrypted_data := UTL_I18N.RAW_TO_STRING(
DBMS_CRYPTO.DECRYPT(
src => v_decrypted_raw,
typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC + DBMS_CRYPTO.PKCS5,
key => v_key
),
'AL32UTF8'
);
END decode_and_decrypt;

-- Public procedure to base64 encode data
PROCEDURE base64_encode(
p_data IN VARCHAR2,
p_encoded_data OUT VARCHAR2
) IS
v_raw_data RAW(32767);
BEGIN
-- Convert string to raw data
v_raw_data := UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8');

-- Base64 encode the raw data
p_encoded_data := base64_encode_raw(v_raw_data);
END base64_encode;

-- Public procedure to base64 decode data
PROCEDURE base64_decode(
p_encoded_data IN VARCHAR2,
p_decoded_data OUT VARCHAR2
) IS
v_decoded_raw RAW(32767);
BEGIN
-- Base64 decode the input data
v_decoded_raw := base64_decode_raw(p_encoded_data);

-- Convert raw data back to string
p_decoded_data := UTL_I18N.RAW_TO_STRING(v_decoded_raw, 'AL32UTF8');
END base64_decode;
END encrypt_decrypt_pkg;
/

Some sample usage code was generated:

-- Example 1: Encrypt and encode data
DECLARE
v_encrypted VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.encrypt_and_encode(
p_data => 'Hello World',
p_encrypted_data => v_encrypted,
p_key => 'my_secret_key'
);
DBMS_OUTPUT.PUT_LINE('Encrypted and encoded: ' || v_encrypted);
END;
/

-- Example 2: Decode and decrypt data
DECLARE
v_decrypted VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.decode_and_decrypt(
p_encrypted_data => 'your_encrypted_string_here',
p_decrypted_data => v_decrypted,
p_key => 'my_secret_key'
);
DBMS_OUTPUT.PUT_LINE('Decrypted data: ' || v_decrypted);
END;
/

-- Example 3: Simple base64 encoding
DECLARE
v_encoded VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.base64_encode(
p_data => 'Hello World',
p_encoded_data => v_encoded
);
DBMS_OUTPUT.PUT_LINE('Base64 encoded: ' || v_encoded);
END;
/

-- Example 4: Simple base64 decoding
DECLARE
v_decoded VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.base64_decode(
p_encoded_data => 'SGVsbG8gV29ybGQ=',
p_decoded_data => v_decoded
);
DBMS_OUTPUT.PUT_LINE('Base64 decoded: ' || v_decoded);
END;
/

As with qwen2.5-coder this qwen3-coder also did not generate the grants needed to run this code.

OK, lets put the 2.5 and 3 code generated into scripts and see if this will compile and run. In both cases I first granted CREATE PROCEDURE, EXECUTE ON DBMS_CRYPTO and EXECUTE ON UTL_ENCODE and created sample users.

I created qwen25.sql and qwen3.sql and added the exact package headers and bodies generated by each LLM model. I have had to add code to create users in each case, either qwen25 or qwen3 and granted privileges to allow the packages to be installed and used. The code for qwen25 for instance to create the user and make grants is:

connect sys/oracle@//192.168.56.34:1521/freepdb1 as sysdba
drop user qwen25 cascade;
create user qwen25 identified by qwen25;
grant create session to qwen25;
grant create procedure to qwen25;
grant execute on dbms_crypto to qwen25;
grant execute on utl_encode to qwen25;

connect qwen25/qwen25@//192.168.56.34:1521/freepdb1
set serveroutput on

I also added show err for each of the package header and body creations for each code generated by the LLM. I also did not include the demos of each package as we are interested initially as to whether the code compiles or not.

Now let us try and run qwen25.sql

SQL> @qwen25
Connected.

User dropped.


User created.


Grant succeeded.


Grant succeeded.


Grant succeeded.


Grant succeeded.

Connected.

Package created.

No errors.

Warning: Package Body created with compilation errors.

Errors for PACKAGE BODY ENCRYPT_DECRYPT_PKG:

LINE/COL ERROR
-------- -----------------------------------------------------------------
21/9 PL/SQL: Statement ignored
21/22 PLS-00306: wrong number or types of arguments in call to
'BASE64_ENCODE'

50/9 PL/SQL: Statement ignored
52/32 PLS-00302: component 'AES256' must be declared
85/9 PL/SQL: Statement ignored
85/38 PLS-00302: component 'RAW_TO_STRING' must be declared
121/9 PL/SQL: Statement ignored
121/36 PLS-00302: component 'RAW_TO_STRING' must be declared
SQL>

The first error at line 21 is because the code shows:

v_encoded := UTL_ENCODE.BASE64_ENCODE(p_data, TRUE);

And UTL_ENCODE.BASE64_ENCODE() only accepts one parameter, the data itself. Even checking Oracle 11 this function did not have an extra parameter. The next error is line 52:

typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC_PKCS5,

The global constants are wrong; for instance DBMS_CRYPTO.AES256 should be DBMS_CRYPTO.ENCRYPT_AES256 and DBMS_CRYPTO.CBC_PKCS5 should probably be DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5.

The next two errors are for RAW_TO_STRING() for instance:

p_decoded_data := UTL_I18N.RAW_TO_STRING(v_decoded_raw, 'AL32UTF8');

The function RAW_TO_STRING() does not exist. It should probably be RAW_TO_CHAR() that returns a VARCHAR2.

OK, so a few errors, let us now try the qwen3 generated code by running my qwen3.sql script:

SQL> @qwen3
Connected.
drop user qwen3 cascade
*
ERROR at line 1:
ORA-01918: user 'QWEN3' does not exist



User created.


Grant succeeded.


Grant succeeded.


Grant succeeded.


Grant succeeded.

Connected.

Package created.

No errors.

Warning: Package Body created with compilation errors.

Errors for PACKAGE BODY ENCRYPT_DECRYPT_PKG:

LINE/COL ERROR
-------- -----------------------------------------------------------------
46/9 PL/SQL: Statement ignored
48/32 PLS-00302: component 'AES256' must be declared
72/9 PL/SQL: Statement ignored
72/38 PLS-00302: component 'RAW_TO_STRING' must be declared
107/9 PL/SQL: Statement ignored
107/36 PLS-00302: component 'RAW_TO_STRING' must be declared
SQL>

Almost the same errors but less; the AES256 constant needs to be changed as well as its compatriots around PKCS5 and CBV the same as the 2.5 model. Also the same RAW_TO_STRING() function needs to be changed.

I am not going to fix them as part of this demo as I have learned enough on this case This was an experiment to see if we can use 3 different LLM models to generate PL/SQL code to encrypt and decrypt data in an Oracle database. A reminder of the prompt I used is here
please write a PL/SQL public package procedures and any supporting private procedures and functions where that public procedure can encrypt and decrypt data passed and base64 encode/decode the result

I thought I was clear; but maybe not. I wanted to have an encrypt() and decrypt() function that privately converted the encrypted data to BASE64 and on decrypt accept BASE64 data and decode it first and then decrypt. In other words only two public functions; encrypt and decrypt that are public. There is a combined function BUT I would have left the BASE64 functions private.

So, we had three LLMs and one failed and fell over; this was qwen3-coder-next. This ran for 51 minutes before falling over completely in the open-webui interface and where it did not generate any code. So this is not included further here.

The qwen2.5-coder LLM took 3723 ms to run and complete generating the code; which is 3.73 seconds. The model was processing at around 24 tokens per second. Not shabby.

The qwen3-coder:30b LLM took 1023 ms which is 1.023 seconds and was processing 134 tokens per second.

In summary, qwen3-coder-next is not useful at least on my mac; it did do a lot of reasoning before it failed so is clearly a better LLM for deeper analysis BUT if it failed then this does not matter. Qwen3-coder:30b ran in time in one third of the time of qwen2.5-coder and in terms of tokens per second it was processing 5.58 tokens per second faster than the 2.5 model.

So, qwen3-coder:30b is clearly faster than qwen2.5-coder and its code that it generated is slightly less broken than the 2.5 model BUT both produced code that did not work. I could have fed the output from each back into the LLM and it may have helped but the purpose of this post was to show two things; 1) the speed of three models compared to each other to generate PL/SQL and the quality of the code.

As I have said in my last blog. I think these models are more useful if you already know what you are doing. They can be productivity assistants and get you some way along the process but the code in each case needed tweaking/fixing but certainly the qwen3-coder:30b is fast enough for daily use.

#oracleace #sym_42 #ai #llm #qwen3-coder #plsql #oracle #security #generate #code

AI

My day job is Oracle security and that covers a massive area of topics from performing security audits, helping customers secure data in their Oracle databases, forensics, building and designing audit trails and reports, secure coding in PL/SQL, encryption of data from TDE to custom native encryption in PL/SQL or using any manor of Oracle cost options such as Database Vault, Virtual Private database and even third party tools.

I love to code and I code in PL/SQL, C, C++, Lua and .net including VB and c++/cli as well as even developing in vba in Excel.

So, AI? - I still use documentation and search engines to find out a setting or command that I cannot remember when developing for Oracle or configuring and setting up something such as Database vault or audit trails but over the last few months I have been also testing and playing with AI; Chatgpt or google Gemini or Microsoft co-pilot. I want to understand what the AI says compared to what I find out myself on Google.

Standard models are good but for specific subjects such as Oracle they often come up with the wrong answers. I can think of some simple examples I have tried for fun. I asked one of the free AIs how to truncate the unified audit trail in Oracle and it said truncate table unified_audit_trail. hmmm. The AI says it with confidence. Instead of saying; i am not sure or I do not know it tells me to truncate the table. I asked another question on changing a database password to the previous one and it showed me using ALTER USER IDENTIFIED BY VALUES but it did not tell how to get the old password first and also used the very old syntax before spare4 was used. I asked it for fun (because I know already) how to connect with a proxy. It just made up the syntax which was wrong. I also asked is Database Vault available with 21c XE and it said no. I knew it was as I had the license pdf open. I corrected the AI and it said - You are right - Oracle Database Vault is included in Oracle 21c XE. My earlier statement was incorrect. I have many many more examples of incorrect answers gathered over a while.

if you know what you are looking for and know what roughly the answer should be then the AI is good because you can recognise if the answer is probably right or not; is it better than the docs or Google; difficult to say; probably not. I know there are better models that you have to pay for but I am from Yorkshire and we are as they say tight. I also know you can augment the AI in three ways to make it better. You can create a well crafted system prompt and add input data via RAG and also use skills.md files to target the right answers.

My fear is the inexperienced person completely relying on the free chat interfaces and then implementing what it tells them in real systems.

We can use AI such as chatgpt as a web based chat window but of course the power of AI is to be able to use things like agents and automate what you want to do; so instead of getting a sample command in a chat window and the using it yourself you can set up AI to run the command on your real systems.

If you do not really know what you are doing in the first place then you should be wary of AI and not simply use the answers.

There has been a lot of noise and chatter about AI for a long time now with some suggesting the industry is a bubble bigger than the dot com bubble and some talking about the huge costs of teaching models that looks unlikely to be recovered by paying customers. There is also the increases in price and shortages of the GPU hardware and the huge energy costs of the data centers.

In the last few days there have been some prominent news articles discussing AI sovereignty and focusing on building AI internally to retain all of the data / prompts passed to the AI. This is exactly paralleled with my Oracle security day job - protecting the your data. As I said in Oracle security in my trainings and here; it is not the job of Oracle to protect your data; it is your job to project your data using the standard features of the Oracle database and even cost options from Oracle or third party solutions. The same applies in AI. If you want to use AI you must protect your data that is fed into the AI .

Is a free on line tool better than a home run LLM (Large Language Model)? this depends on a lot of things; the hardware that you use, the sources of data that you can add to it via RAG or the context window and of course the model you download and use.

My experience so far with the free tools is that you get blocked quickly particularly if you upload files. Load 2 and you are fine, load three and it says come back tomorrow in 24/25 hours and try again.

I have been playing with a local LLM recently as well to test how good it is. I am feeding it skills files and sources via RAG and testing is in early stages but looks great so far. I m using a Mac Book Pro M5 with 64gb of RAM and a 2 TB SSD. There are so many models to use so I will be trying quite a few. I have tested qwen2.5 coder so far. My experience of setting up the local LLM was great, downloading the models takes the biggest time but setting up to run with a chat interface was simple.

More on this soon as I will report back here on my experience with a local LLM. Having a local LLM that does not have limits and is almost as fast as chatgpt and the answers are at a similar level so far is good.

#oracleace #sym_42 #ai #llm #localllm #oracle #security