Hiding Encryption and Credit Card Numbers In Plaintext With Format Preserving Encryption

Prof Bill Buchanan OBE
5 min readNov 12, 2023

--

We can normally spot when something is encrypted, as it either looks like Base64 or hex characters. But, why can’t we convert our ciphertext into a form that looks a bit more like the characters we would expect to see? And, could we obfuscate our credit card details into a form that still looks like a credit card, but which has actually been encrypted, and only with a secret password can we reveal the real credit card number? Well, we can do this, and the magic method is Format Preserving Encryption (FPE). In this example, we will define a character set for the output format, and generate a random key, and then use C# coding.

Outline

Within tokenization we can apply format-preserving encryption (FPE) methods, which will convert our data into a format which still looks valid, but which cannot be mapped to the original value. For example, we could hide Bob’s credit card detail into another valid credit card number, and which would not reveal his real number. A tokenization server could then convert the real credit card number into a format which still looked valid. For this, we have a key which takes the data and then converts it into a form which the same length as the original.

The method we use is based on a Feistel structure, and where we have a number of rounds, and then apply the key through a Feistel function for each round:

We thus split the data into blocks (typically 64-bits), and then split it into two parts. We then take these splits into the left part and the right part, and feed through each round, and then swap them over. The ⊕ symbol is an exclusive-OR operator.

So, we have a problem here. In most encryption methods we deal with block sizes, such as 64 bits for DES and 128 bits for AES. The output will then be a multiple of 64 bits or 128 bits, as we cipher one block at a time. In FPE we want to have something which will match to the length of the input data. The solution is Format-preserving, Feistel-based encryption (FFX) and which produces an output which matches the length of the input.

NIST has thus defined a standard known as SP 800–38G, and which defines two FF schemes: FF1 an FF3. While these work on 128-bit block sizes, they can also work on blocks which have fewer bits than this. For this we have a key (K) and which creates a permutation of the bits to create an invertible version of the output.

For FF1 we have 10 rounds and for FF3 we have eight rounds. First, we split an input value of n characters into a number of characters (u and v — and where n = u + v):

For the encrypting process, we use a modular addition (EX-OR) and for decryption, we use a modular subtraction. For each round, we split into a and b. For the F function in each round, we generate an HMAC output (using SHA-1) from the key (K), the bᵢ, and the counter value (i):

h = hmac.new(self.key, key + struct.pack('I', i), self.digestmod)

and where self.key (K) is the key (normally a passphrase) that we will use to make the conversion, key is the bi input, and self.digestmod is defined as hashlib.sha1. This output will then either be added (encryption) or subtracted (decryption) to the ai input.

An important parameter is the radix value, and which defines the total number of characters that we will use for the character set. If it is binary, we will have a value of 2, if it is hexadecimal characters the value will 16, and for lower case characters it will be 26.

For encryption we just modular add our current value of a to the output of the key round (h) and swap values:

c = self.add(radix, a, self.round(radix, i, b))
a, b = b, c

For decryption we just modular subtract our current value of a from the output of the key round (h) and swap values:

c = self.sub(radix, a, self.round(radix, i, b))
a, b = b, c

Coding

An outline of the code is [here]:

namespace FPE
{
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Fpe;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Utilities;
using Org.BouncyCastle.Security;
using System.Text.Json;

class Program
{

static void Main(string[] args)
{



var alpha= "abcdefghijlmnopqrstuvwxyz";
var msg ="hello";

if (args.Length >0) msg=args[0];
if (args.Length >1) alpha=args[1];

try {


var alphabet=alpha.ToCharArray();
var plainTextData=msg.ToCharArray();


// Random key generation
CipherKeyGenerator myKey = new CipherKeyGenerator();
myKey.Init(new KeyGenerationParameters(new SecureRandom(),128));
var keyParam = myKey.GenerateKeyParameter();
// Create a mapper from our alphabet to indexs
IAlphabetMapper alphabetMapper = new BasicAlphabetMapper(alphabet);
// Create FpeParameter object
byte[] tweak = System.Text.Encoding.ASCII.GetBytes("0123456");
FpeParameters fpeKeyParam = new FpeParameters(keyParam, alphabetMapper.Radix, tweak);
IBlockCipher cipher = new AesEngine();
FpeFf3_1Engine cipherMode = new FpeFf3_1Engine(cipher);
cipherMode.Init(true,fpeKeyParam);
byte[] cipherTextData = new byte[plainTextData.Length];
byte[] convertedPlainTextData = alphabetMapper.ConvertToIndexes(plainTextData);
int result = cipherMode.ProcessBlock(convertedPlainTextData, 0,convertedPlainTextData.Length, cipherTextData, 0);
char[] convertedCipherTextData = alphabetMapper.ConvertToChars(cipherTextData);
//var fpe=convertedCipherTextData;
// Decipher
cipherMode.Init(false,fpeKeyParam);
byte[] plainText = new byte[cipherTextData.Length];
byte[] convertedCipherText= alphabetMapper.ConvertToIndexes(convertedCipherTextData);
result = cipherMode.ProcessBlock(convertedCipherText, 0,convertedCipherTextData.Length, plainText, 0);
var plain = alphabetMapper.ConvertToChars(plainText);

Console.WriteLine("== Format Preserving Encryption ==");
Console.WriteLine("Key: {0}",Convert.ToHexString(keyParam.GetKey()));
Console.WriteLine("Key size: {0}",keyParam.GetKey().Length);
Console.WriteLine("\nCiphered: {0}", new String(convertedCipherTextData));
Console.WriteLine("\nDceiphered: {0}", new String(plain));

} catch (Exception e) {
Console.WriteLine("Error: {0}",e.Message);
}
}
}
}

A sample run is with an alphabet of “0123456789” [here]:

== Format Preserving Encryption ==
Message: 378734493671000
Alphabet: 0123456789
Key: 3C27E645FB83EFEDE7BEEA8C4ADD42101B1D6D298AB7870D81C707CE07CFA733
Key size: 32
Ciphered: 035363885402584
Dceiphered: 378734493671000

and for an alphabet of “0123456789abcdef” [here]:

== Format Preserving Encryption ==
Message: 64de30
Alphabet: 0123456789abcdef
Key: 924662950B0187813CEE04BE79D0061EFA79438B3C5A0212821F29CB4F09FB87
Key size: 32
Ciphered: 35bf76
Deciphered: 64de30

and for an alphabet of “*$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ” [here]:

== Format Preserving Encryption ==
Message: $qwerty*
Alphabet: *$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
Key: 59C5E3375791CA1719E3331A9CF84D8FE7111691F60F34D55E2E34950446CCA0
Key size: 32
Ciphered: IlhFaAfn
Dceiphered: $qwerty*

The following are a few examples:

  • Am Ex: “‎378734493671000” (p/w=pass123, alpha=”0..9"). Try.
  • Switch/Solo: “‎6331101999990016” (p/w=pass123, alpha=”0..9"). Try.
  • Visa: “‎4111111111111111” (p/w=pass123, alpha=”0..9"). Try.
  • MasterCard of “‎5105105105105100” (p/w=pass123, alpha=”0..9"). Try.
  • Diners Club: “‎38520000023237” (p/w=qwerty, alpha=”0..9"). Try.
  • SSN: “‎575701423” (p/w=qwerty, alpha=”0..9"). Try.
  • A hex value of “‎64de30” (p/w=hello, alpha=”0..f”). Try.
  • A string of “‎billbuchanan” (p/w=test, alpha=”a…z and space”). Try.
  • A PIN number of “‎8120” (p/w=test, alpha=”0..9"). Try.
  • A password of of “‎$Qwerty*” (p/w=test, alpha=”a..z”). Try.

Conclusions

And there you go. You can easily create any format that you require for your output, and where you can hide the underlying data. There are more examples here:

https://asecuritysite.com/fpe

--

--

Prof Bill Buchanan OBE

Professor of Cryptography. Serial innovator. Believer in fairness, justice & freedom. Based in Edinburgh. Old World Breaker. New World Creator. Building trust.