Because the password probably shouldn't be in the CL source
If you work on IBM i in a shop that's been around for a while, there's a good chance a password is sitting in clear text in a source member right now. Probably more than one. It might be an FTP password, a database connection string or a vendor API key. It might be in a data area whose authority was never locked down, or in a CL program that anyone with read access to the library can display.
That usually isn't carelessness. It's how things were done for a long time, and nobody has had the time to go back and clean it up.
This post is about how I finally cleaned it up: how I store and use credentials in a production framework on IBM i, using the database's own SQL encryption functions, without adding any new technology to the stack.
Why this is harder than it looks
When you first sit down to "encrypt the password," a few bad ideas tend to come up:
- Base64 or a simple XOR. That isn't encryption. Anyone with read access can decode it.
- Hashing it. Hashing is one-way, and you can't log into an FTP server with a hash. You need the plaintext at the moment you use it.
- A third-party encryption library. Now you have a dependency, a licence question and someone else's code running in your service program, and you don't need any of that.
- A key hardcoded in source. Then the key is in the source, which is the same problem one level down.
What you actually need is:
- Real encryption at rest, meaning AES and not a scheme you invented.
- A key stored separately from both the encrypted data and the source, with tight authority on it.
- Decryption only at the moment of use, so the plaintext is in memory as briefly as possible.
- The plaintext never written anywhere else: not a log, a spool file, an error message, the operator message queue or an audit record.
IBM i has had everything needed for this for years. It just has to be put together.
The tools are already in the database
You don't need to call a cryptographic API for this. DB2 for i provides ENCRYPT_AES256 and DECRYPT_CHAR as SQL scalar functions, and you can call them from embedded SQL like any other function. There are no algorithm descriptors or key structures to build by hand.
VALUES ENCRYPT_AES256(:clearText, :key, :hint)
You pass in the clear text and a key and get ciphertext back. DECRYPT_CHAR does the reverse: pass it the ciphertext and the same key, and you get the clear text back. It's AES-256 underneath, and none of the cipher code is mine.
There's one catch. ENCRYPT_AES256 returns binary data, and storing arbitrary binary in a CHAR column causes trouble as soon as something treats it as text, whether that's a display, a copy between CCSIDs or a comparison. So the result goes through BASE64_ENCODE before it's stored, and through BASE64_DECODE before it goes back into DECRYPT_CHAR. Those are two more built-in functions, so there's still nothing of my own to maintain.
You don't need to write a cipher, and you shouldn't want to. In this case you don't even need to call a crypto API. The database already has what you need, and SQL is how you get to it.
Where the key lives
This is where a lot of implementations go wrong, so it's worth being careful.
The key isn't in the source code, it isn't in a physical file, and it isn't in the same library as the config table it protects. It's in a data area with its authority locked down.
Setting it up involves three things:
- A 32-byte character data area. AES-256 needs a 256-bit key, which is 32 bytes.
- Public authority set to exclude. By default nobody can read it: not end users, not developers.
- An explicit grant to the service program's user profile. The encryption service program runs under a specific profile that has read access to the data area, and nothing else does.
If your service program uses adopted authority instead, with the owner having read access to the key data area, you get the same result. The service program can read the key, but the calling program and the calling user can't.
This is something IBM i is genuinely good at, and we often forget to use it. Object-level authority on a single data area is stricter access control than a lot of cloud secret managers give you by default. It just looks unfamiliar because most of what we read about secrets management is about vault products.
The encryption service program
The core of this is a small service program with two exported procedures:
- Encrypt takes the clear text (up to 64 characters, which is plenty for a password) and returns the BASE64-encoded ciphertext, ready to store.
- Decrypt takes the stored value and returns the clear text.
That's the entire public interface. Everything else is internal to those two procedures.
Inside Encrypt
The real procedure is only a couple of dozen lines, because nearly all the work happens inside the SQL function call:
dcl-proc Encrypt export;
dcl-pi *n char(256);
plaintext varchar(64) const;
end-pi;
dcl-s result char(256);
dcl-s clearBuf varchar(64);
dcl-s keyBuf char(32);
dcl-s hint char(5) inz('APP01');
in *lock APPKEYDA; // native op-code read, not an API call
keyBuf = APPKEYDA;
unlock APPKEYDA;
clearBuf = plaintext;
exec sql
VALUES(BASE64_ENCODE(ENCRYPT_AES256(:clearBuf, :keyBuf, :hint)))
INTO :result;
clear keyBuf;
clear clearBuf;
return result;
end-proc;
Two things are worth pointing out.
There's no IV to generate. ENCRYPT_AES256 handles that internally and includes it in what it returns, so there's no random-number call, no algorithm descriptor and no manual concatenation.
IN *LOCK and UNLOCK are native RPG op-codes, not an API call. Reading a data area is about as simple as reading a value gets on this platform. The hint parameter is part of ENCRYPT_AES256's signature. DB2 stores it with the ciphertext so GETHINT() can return it later without decrypting anything. I don't use it for anything here, so it's just a fixed literal, but the function expects it.
Decrypt is the same thing in reverse. It reads the key the same way, runs BASE64_DECODE on the stored value, passes that to DECRYPT_CHAR with the key, clears the key from the local variable and returns the result.
Using it safely at the call site
This part matters more than the encryption itself.
Here's how the execution program, the one that actually does the FTP transfer, uses a stored credential:
// 1. Read the encrypted password from the config table
// 2. Decrypt. The plaintext now exists in a local field
// 3. Use it immediately. The login call is the only thing
// that sees it
// 4. Clear it from memory as soon as we're done
Those four steps always happen in that order. The plaintext is in memory for the length of the login call and is cleared as soon as the login returns, whether it worked or not. The clear-text field never leaves the procedure.
What's deliberately left out:
- The plaintext is never displayed.
- No log line includes the password. That should go without saying, but I've seen it happen.
- It's never copied into a global field for later use. There is no later use.
- No error message includes the password. If the login fails, the message says the login failed for that user. The password isn't the problem; whatever the server sent back is.
- No audit record holds the plaintext. The audit table logs changes to the encrypted field as asterisks, every time. If you need to show that a password changed, you can see when and by whom, but not the value, which is how it should be.
If I could change one habit in IBM i shops, it would be logging the password "just for debugging, I'll take it out later." It won't get taken out, and it'll sit in a spool file for three years.
Key rotation
You need a plan for this before you deploy, not after.
Here's mine:
- Generate a new 32-byte key and keep it somewhere outside IBM i for the duration of the rotation, such as a password manager or whatever your compliance rules allow.
- For each row in the config table, read the stored value, decrypt it with the old key, re-encrypt it with the new key and write it back.
- Update the data area with the new key.
- Destroy the outside copy of the new key.
The rotation itself is a one-off program that takes the old and new keys as parameters and runs once. You delete it afterwards rather than leaving it around.
A more elaborate version keeps a key ID column next to each encrypted value, so more than one key can be in use during a rotation window. For most shops, rotating everything at once is fine. It takes a couple of seconds even with thousands of rows.
Other things to watch for
Even with the encryption done properly, I've seen people trip over these:
The password as a job parameter. If you submit a job with a clear-text password as a parameter, it shows up in the job's details and in the job log. Never pass a plaintext password as a parameter.
The password in a temporary physical file. Staging it in a work file "just for a second" is a bad idea. Work files stay around until they're cleared, and longer if the job crashes.
The password in a user space. User spaces can be read by anyone with the right authority. If you wouldn't write the password to a physical file, don't write it to a user space either.
Adopted authority in the wrong place. If your service program adopts authority to read the key data area, make sure the programs that call it don't adopt it too. The point is that only one narrow piece of code can see the key.
Forgetting to clear the field. In RPG, local fields go out of scope when the procedure ends, and their memory gets reused eventually. The problem is "eventually." A failed login could leave the plaintext in the activation group's memory until something else overwrites it. Clearing the field explicitly as the last step costs almost nothing.
Why it's worth doing even if nobody asks
Nobody is auditing your IBM i shop for clear-text FTP passwords. A security team won't find them unless they go looking, and application scanners won't flag them because they don't scan RPG source.
So this is something you either fix because you care about it, or you don't fix at all.
The practical argument is that when someone does ask, whether because of a breach, an audit, a new vendor or a merger, the fix takes an afternoon if you've already built the pattern and months if you haven't. In my current framework the encryption service program is about a page of RPG, two procedures that mostly call SQL. Creating the data area is one command, and locking down its authority is another. Every program that uses a credential already goes through Encrypt and Decrypt, so rotating the key is one program and about fifteen minutes.
There's nothing clever or new in any of this. DB2 for i has had these encryption functions for years.
Wrapping up
The short version: on IBM i, a password should live in an encrypted field in a config table. It's encrypted with an AES-256 key kept in a data area with locked-down authority, decrypted by a small service program only when it's needed, cleared from memory right afterwards, and never written anywhere else.
You don't need a vault, a new tool or anything from outside the platform. The encryption functions are already in the database.
I use this pattern for every credential in the FTP framework I rebuilt from a pile of ad-hoc scripts. I wrote about that framework in I Rebuilt Our FTP Layer on IBM i, and the code where the credential is decrypted and used is in The FTP Execution Engine, Line by Line. It's a few dozen lines of RPG and a data area, and it's the cheapest lasting security improvement I've made to an IBM i codebase in years.
Once you do this for one program, you'll probably end up doing it for others. More importantly, the next time you open a CL program and see a password in plain text, you'll notice it and fix it, and that habit matters more than the encryption.
Updated November 2026: the first version of this post described building the encryption by hand with the IBM i Cryptographic APIs, including generating and storing an IV. That isn't how the production code works. It uses DB2 for i's ENCRYPT_AES256 and DECRYPT_CHAR SQL functions with BASE64 encoding, and the database handles the IV. The code and explanation have been corrected, and the post now links to the FTP articles that use this pattern.
Jaya Krushna Mohapatra is a Warehouse Management Systems Architect focused on enterprise integrations, IBM i modernization, and scalable backend systems.
Top comments (0)