If using an older version of the Oracle database (11gR1 or earlier) the UTL_SMTP package does not have authentication APIs built in. You may also run into a situation where a server implements a mechanism that UTL_SMTP does not support. You still have options though, by implementing some of the authentication means manually.
Discussing the implementations of all possibilities is beyond the scope of this article and some aren’t possible to replicate solely within the UTL_SMTP API; but I will show a few of the more common methods and a framework that could be used to write your own if needed.
Initiating the Communication
DECLARE v_connection UTL_SMTP.connection; v_replies UTL_SMTP.replies; v_reply UTL_SMTP.reply; BEGIN v_connection := UTL_SMTP.open_connection(host => 'smtp.example.com', port => 587 ); v_replies := UTL_SMTP.ehlo(v_connection, 'example.com'); FOR i IN 1 .. v_replies.COUNT LOOP DBMS_OUTPUT.put_line( 'EHLO Reply: ' || v_replies(i).code || ' ' || v_replies(i).text); END LOOP; v_reply := UTL_SMTP.quit(v_connection); DBMS_OUTPUT.put_line('QUIT Reply: ' || v_reply.code || ' ' || v_reply.text); END;
Since authentication is an extension to base SMTP functionality, you will initiate communication with the EHLO (extended hello) command rather than the standard HELO command.
When the server reads an EHLO command its response should include the types of authentication supported, if any. The response might look something like this:
EHLO Reply: 250 AUTH LOGIN PLAIN CRAM-MD5 CRAM-SHA1
EHLO Reply: 250 STARTTLS
This would indicate the server supports 4 methods of SMTP authentication as well as encryption through SSL/TLS.
First, let’s look at the SMTP authentication methods. The first 3 of those listed (LOGIN, PLAIN, and CRAM-MD5) are supported by UTL_SMTP in 11gR2 and above. All of these include Base-64 Encoding, as part of their algorithms. It is important to note, Base-64 Encoding is NOT encryption. It’s simply a text conversion to a subset of ASCII for purposes of ensuring correct processing regardless of character sets. Encoding is unkeyed and completely reversible. As such it should be considered “plain text” for purposes of security evaluation of the PLAIN and LOGIN methods.
AUTH PLAIN
With this method the client sends the authenticating id, the user id and the password as a single Base-64-encoded, NULL-delimited (ASCII-0) string. For most email servers, the authenticating id and the user id will be the same.
If they differ, the details should be provided by your server as to what the required id’s are.
With PLAIN, there is no prompting or challenge, you simply send an authentication command and the server will process it. The syntax is a single line, and might look something like this…
AUTH PLAIN bXlfdXNlcl9uYW1lAG15X3VzZXJfbmFtZQBteV9zZWNyZXRfcGFzc3dvcmQ=
which would look like this after decoding with “|” representing the NULL character – chr(0)
AUTH PLAIN my_user_name|my_user_name|my_secret_password
To replicate this functionality in PL/SQL, you simply send these strings with the UTL_SMTP.COMMAND procedure after connecting.
v_plain_string :=
UTL_RAW.cast_to_varchar2(
UTL_ENCODE.base64_encode(UTL_RAW.cast_to_raw('my_user_name'||chr(0)||'my_user_name'||chr(0)||'my_secret_password'))
);
v_connection := UTL_SMTP.open_connection(:v_smtp_server);
UTL_SMTP.ehlo(v_connection, 'mydomain.com'); -- Must use EHLO vs HELO
UTL_SMTP.command(v_connection, 'AUTH', 'PLAIN ' || v_plain_string);
AUTH LOGIN
Another authentication method, LOGIN, will prompt you for username and password responses. The prompts will be Base-64 encoded and the responses are expected to be as well. SMTP authentication via LOGIN method with a telnet session might look something like this…
AUTH LOGIN
334 VXNlcm5hbWU6
bXlfdXNlcl9uYW1l
334 UGFzc3dvcmQ6
bXlfc2VjcmV0X3Bhc3N3b3Jk
235 Authed. Go on.
Which would like this after decoding
AUTH LOGIN
334 Username:
my_user_name
334 Password:
my_secret_password
235 Authed. Go on.
To replicate this functionality in PL/SQL, you simply send these strings with the UTL_SMTP.COMMAND procedure after connecting. As with AUTH PLAIN you initiate communication with the EHLO command and use UTL_SMTP.COMMAND to send the authentication instructions but rather than putting all of the authentication information on one line, the server will prompt you as shown above. In this example, if the login, user or password messages aren’t accepted, an error should be raised from the server which will then be re-raised by UTL_SMTP as an oracle UTL_SMTP exception.
DECLARE v_connection UTL_SMTP.connection; v_replies UTL_SMTP.replies; v_reply UTL_SMTP.reply; v_username_b64 VARCHAR2(1000); v_password_b64 VARCHAR2(1000); BEGIN v_username_b64 := UTL_RAW.cast_to_varchar2( UTL_ENCODE.base64_encode(UTL_RAW.cast_to_raw( :v_username))); v_password_b64 := UTL_RAW.cast_to_varchar2( UTL_ENCODE.base64_encode(UTL_RAW.cast_to_raw( :v_password))); v_connection := UTL_SMTP.open_connection( :v_smtp_server, 587); v_replies := UTL_SMTP.ehlo(v_connection, 'wowway.com'); FOR i IN 1 .. v_replies.COUNT LOOP DBMS_OUTPUT.put_line( 'EHLO Reply: ' || v_replies(i).code || ' ' || v_replies(i).text); END LOOP; -- should receive a 334 response, prompting for username v_reply := UTL_SMTP.command(v_connection, 'AUTH', 'LOGIN'); DBMS_OUTPUT.put_line( 'Command AUTH Reply: ' || v_reply.code || ' ' || v_reply.text); -- should receive a 334 response, prompting for password v_reply := UTL_SMTP.command(v_connection, v_username_b64); DBMS_OUTPUT.put_line( 'COMMAND Username Reply: ' || v_reply.code || ' ' || v_reply.text); -- should receive a 235 response, you are authenticated v_reply := UTL_SMTP.command(v_connection, v_password_b64); DBMS_OUTPUT.put_line( 'COMMAND Password Reply: ' || v_reply.code || ' ' || v_reply.text); v_reply := UTL_SMTP.quit(v_connection); DBMS_OUTPUT.put_line('QUIT Reply: ' || v_reply.code || ' ' || v_reply.text); END;
EHLO Reply: 250 smtp.example.com says EHLO EHLO Reply: 250 STARTTLS EHLO Reply: 250 AUTH LOGIN Command AUTH Reply: 334 bXlfdXNlcl9uYW1l COMMAND Username Reply: 334 bXlfc2VjcmV0X3Bhc3N3b3Jk COMMAND Password Reply: 235 Authed. Go on. QUIT Reply: 221 smtp.example.com closing connection
AUTH CRAM-MD5 (or CRAM-SHA1)
The Challenge-Response Authentication Mechanism (CRAM) with MD5 is defined in RFC2195 ( http://www.ietf.org/rfc/rfc2195.txt ) This is the first authentication method described that truly masks the password. The details of the mechanism are known as Hash-based Message Authentication Code (HMAC defined in RFC2104) and are fairly convoluted; but the basic idea is the server will send a Base-64 encoded “challenge”. The client will then generate a hash of a shared, secret string (i.e. your password) with some bit manipulations and then rehash the hashed-password with the challenge and then finally append that to the username. That entire string is then Base-64 encoded and sent to the server. The server uses the same mechanism to generate the hash and compares to the hash the client sent. If they match then the user is authenticated. Note, while hashing is irreversible, it’s still not cryptographically impervious. MD5 is known to be less secure than SHA-1, so some servers may authenticate with CRAM-SHA1 instead of CRAM-MD5. The process is identical except for the choice of hash algorithm. RFC4954 ( http://www.ietf.org/rfc/rfc4954.txt ) illustrates use of CRAM-MD5 when applied to SMTP communication.
Using the Sample challenge, username and password from RFC2195 the CRAM-MD5 algorithm might look something like this…
The server sends challenge: “PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+” (quotes not included)
Which is “<1896.697170952@postoffice.reston.mci.net>” when decoded (quotes not included)
SELECT UTL_RAW.cast_to_varchar2(
UTL_ENCODE.base64_encode(
UTL_RAW.cast_to_raw(
'tim ' -- username
|| LOWER(
RAWTOHEX(
DBMS_CRYPTO.hash(
UTL_RAW.CONCAT(
UTL_RAW.bit_xor(secret, UTL_RAW.copies(HEXTORAW('5C'), 64)),
DBMS_CRYPTO.hash(
UTL_RAW.CONCAT(
UTL_RAW.bit_xor(
secret,
UTL_RAW.copies(HEXTORAW('36'), 64)
),
UTL_ENCODE.base64_decode(
UTL_RAW.cast_to_raw( -- challenge
'PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+'
)
)
),
2 /* 2= MD5 */
)
),
2 /* 2= MD5 */
)
)
)
)
)
)
encoded_cram
FROM (SELECT UTL_RAW.overlay(
UTL_RAW.cast_to_raw('tanstaaftanstaaf'), -- password
UTL_RAW.copies(HEXTORAW('00'), 64)
)
secret
FROM DUAL);
The above is a bit hard to follow, and it doesn’t take into account requirements for handling passwords over 64 bytes. So, I have provided a function below that will encapsulate the full CRAM algorithm, including support for long passwords as well as the option to switch to CRAM-SHA1. Using this function makes it much easier to generate the proper response to the challenge string.
SELECT sdscram(
'tim', --username
'tanstaaftanstaaf', -- password
'PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+' -- challenge from server
)
FROM DUAL;
expected result:
dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw
CREATE OR REPLACE FUNCTION sdscram(p_username IN VARCHAR2,
p_key IN VARCHAR2,
p_challenge IN VARCHAR2,
p_hashmethod IN INTEGER DEFAULT DBMS_CRYPTO.hash_md5
)
RETURN VARCHAR2
IS
/*
Challenge-Response Authentication Mechanism
as in RFC 2195
http://www.ietf.org/rfc/rfc2195.txt
PL/SQL implementation by Sean D. Stuber
example usage with data shown in RFC 2195 doc
select
sdscram(
'tim',
'tanstaaftanstaaf',
'PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+'
)
from dual;
expected result:
dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw
*/
c_blocksize CONSTANT INTEGER := 64;
c_zeroblock CONSTANT RAW(64) := UTL_RAW.copies(HEXTORAW('00'), c_blocksize);
c_outerpad CONSTANT RAW(64) := UTL_RAW.copies(HEXTORAW('5C'), c_blocksize);
c_innerpad CONSTANT RAW(64) := UTL_RAW.copies(HEXTORAW('36'), c_blocksize);
v_outer RAW(64);
v_inner RAW(64);
v_challenge VARCHAR2(32767);
v_key RAW(32767) := UTL_RAW.cast_to_raw(p_key);
v_hash VARCHAR2(64);
BEGIN
-- If the key is bigger than the block size (64) then hash it
-- which will reduce it to a shorter byte stream
IF UTL_RAW.LENGTH(v_key) > c_blocksize
THEN
v_key := DBMS_CRYPTO.hash(v_key, p_hashmethod);
END IF;
-- 0-pad the key to fill a block
IF UTL_RAW.LENGTH(v_key) < c_blocksize
THEN
v_key := UTL_RAW.overlay(v_key, c_zeroblock);
END IF;
-- The challenge will be a base64 encoded string
v_challenge := UTL_ENCODE.base64_decode(UTL_RAW.cast_to_raw(p_challenge));
v_outer := UTL_RAW.bit_xor(v_key, c_outerpad);
v_inner := UTL_RAW.bit_xor(v_key, c_innerpad);
-- append the challenge to the key and hash it
v_inner := DBMS_CRYPTO.hash(UTL_RAW.CONCAT(v_inner, v_challenge), p_hashmethod);
-- append the new inner hash to the outer and hash again
-- return results as a string for use in next step
v_hash :=
LOWER(RAWTOHEX(DBMS_CRYPTO.hash(UTL_RAW.CONCAT(v_outer, v_inner), p_hashmethod)));
-- base64 encode the username with the hash, separated by a space
RETURN UTL_RAW.cast_to_varchar2(
UTL_ENCODE.base64_encode(UTL_RAW.cast_to_raw(p_username || ' ' || v_hash)));
END;
example use might look something like this
DECLARE
v_connection UTL_SMTP.connection;
v_reply UTL_SMTP.reply;
v_cram_string VARCHAR2(4000);
BEGIN
v_connection := UTL_SMTP.open_connection( :v_smtp_server, 587);
UTL_SMTP.ehlo(v_connection, 'mydomain.com');
v_reply := UTL_SMTP.command(v_connection, 'AUTH', 'CRAM-MD5');
IF v_reply.code = 334
THEN
v_cram_string := sdscram( :v_username, :v_password, v_reply.text);
v_reply := UTL_SMTP.command(v_connection, v_cram_string);
DBMS_OUTPUT.put_line(
'COMMAND Cram Response Reply: '
|| v_reply.code
|| ' '
|| v_reply.text);
END IF;
v_reply := UTL_SMTP.quit(v_connection);
DBMS_OUTPUT.put_line('QUIT Reply: ' || v_reply.code || ' ' || v_reply.text);
END;
If the server supported CRAM-SHA1, the usage would be the same as above except when calling the SDSCRAM function, the optional 4th parameter p_hashmethod would be set to DBMS_CRYPTO.HASH_SH1.
As of Oracle version 11.2.0.2, STARTTLS is supported. If your server supports it, it will generally be better to use it before sending hashes, and definitely before sending plain text or base64-encoded credentials. If however, you are using 11.2.0.2 or higher, you should not only use STARTTLS, but the AUTH command itself will be available within UTL_SMTP, thus negating the need to use the manual steps described above.
If your server implements a new algorithm, such as salted challenge-response mechanism (SCRAM) the general flow will be the same as shown above. Open a connection (with or without TLS,) initiate handshake with EHLO, use the COMMAND routine to send manual AUTH statement and check the replies to pick up your salt and iterations, encode your credentials, and then reply.
Other algorithms will also have similar rules. The EHLO command will list the supported algorithms with the AUTH tag in the response. The specific challenges, responses, encryptions, and encodings will, of course, be dependent on the specified algorithm but the SMTP communication itself will still follow the same general flow.
If you encounter such servers, I hope these examples help. As always, questions and comments are welcome.