Reusing the idea from here, though in this case the REGEXP_REPLACE() approach from MTO is definitely the best one.
First we generate CTE as a number table with integer from 0 to the maximum of plaintext characters of a password minus 1.
We can get the number of cleartext characters in an obfuscated string as follows:
- We can ignore the first 3 characters, those will always be garbage.
If the number of plaintext characters is even, there will be 7 characters for each 2 plaintext characters, because the total of the garbage characters is 5 for 2 plaintext characters.
So we get the number of plaintext characters with FLOOR((LENGTH(PASSWORDS.PASSWORD) - 3) / 7) * 2 if it is even.
If it is odd, it is one more than an even number of plaintext characters and the length of the string's length minus 3 is no longer divisible by 7, because the last 1 plaintext character will be followed by 2 garbage characters.
So we can check for the string length minus 3 modulo 7. If it is 0 the number of plaintext characters is even, we don't need to add anything. If it isn't 0, we add 1 and get the total (odd) number of plaintext characters.
Together that's FLOOR((LENGTH(PASSWORDS.PASSWORD) - 3) / 7) * 2 + DECODE(MOD(LENGTH(PASSWORDS.PASSWORD) - 3, 7), 0, 0, 1).
We left join that CTE to the passwords table, so that for each cleartext character of a password there is a row with the password and a number from 0 to the number of cleartext characters of the password.
We can now use SUBSTR() to get each cleartext character. The base offset is 4, as we can ignore the first three characters. The joined number from the CTE let us calculate the additional offset. We always advance by at least 3 characters, which gives us CTE.I * 3. Additionally every 2 cleartext characters we need to further advance 1 time, so we add FLOOR(CTE.I / 2) giving us SUBSTR(PASSWORDS.PASSWORD, 4 + CTE.I * 3 + FLOOR(CTE.I / 2), 1).
No we have every single cleartext character, but in different rows. To concatenate them back together we group by the obfuscated password (and possibly by an ID too, should there be more then one row in the base table with the same password) and use LISTAGG. Ordering the the number from the CTE makes sure every plaintext character gets the right position.
WITH CTE(I)
AS
(
SELECT 0 I
FROM DUAL
UNION ALL
SELECT CTE.I + 1
FROM CTE
WHERE CTE.I + 1 < (SELECT MAX(FLOOR((LENGTH(PASSWORDS.PASSWORD) - 3) / 7) * 2
+ DECODE(MOD(LENGTH(PASSWORDS.PASSWORD) - 3, 7),
0, 0,
1))
FROM PASSWORDS)
)
SELECT PASSWORDS.ID,
PASSWORDS.PASSWORD PASSWORD_OBFUSACTED,
LISTAGG(SUBSTR(PASSWORDS.PASSWORD, 4 + CTE.I * 3 + FLOOR(CTE.I / 2), 1))
WITHIN GROUP (ORDER BY CTE.I) PASSWORD_CLEARTEXT
FROM PASSWORDS
LEFT JOIN CTE
ON CTE.I < FLOOR((LENGTH(PASSWORDS.PASSWORD) - 3) / 7) * 2
+ DECODE(MOD(LENGTH(PASSWORDS.PASSWORD) - 3, 7),
0, 0,
1)
GROUP BY PASSWORDS.ID,
PASSWORDS.PASSWORD;
db<>fiddle
Note: This demonstrates, that an attacker must not even guess the random character (or characters, even that wouldn't make a difference), to get the cleartext password. Ergo this is an unsafe method to store passwords! Use hashing instead, that's (most likely, of course depending on the algorithm) irreversible.
substr().