Encrypting credit card numbers in a database

You need to encrypt a set of clients' credit card numbers (or social security numbers, or tax file numbers, or some other field that needs to be kept secret) in a database along with their other details.

Here's how not to do it, followed by some better methods.

Method 1

Let's start with a simple database table of clients with their credit card numbers.

ClientIDClientNameCreditCardNum
1John Doe1234-5678-9789-0124
2Fred Bloggs1234-5671-9988-7766
3Erika Mustermann3456-7898-9789-0124

Well, you could just store it like that. Frighteningly, lots of big stores seem to do that. They may have a secure SSL connection for you to submit your credit card number, and, yes, it is impossible for anyone monitoring your internet connection at that time to get your credit card number, but then the silly idiots go and store it on their database with your number in the clear for any disgruntled employee or hacker to find. DO NOT DO THIS!.

Method 2

Database systems like Microsoft Access have an option to format a particular field in "password mode", so, when viewed, the table looks like

ClientIDClientNameCreditCardNum
1John Doe*******************
2Fred Bloggs*******************
3Erika Mustermann*******************

This might look good to a casual onlooker, but anyone with any knowledge of the database can extract the underlying data without a problem. DO NOT DO THIS!.

Method 3

OK, so you decide to encrypt the data. Use a symmetric encryption algorithm like AES or Triple DES to do this. Let's leave aside the issues involved in keeping the key secret - we will assume here that you have handled that independently and that a suitable secret key is available to encrypt and decrypt data. In the examples that follow, we'll use Triple DES (a.k.a. 3DES or TDEA) with the 192-bit key
000102030405060708090A0B0C0D0E0F1011121314151617
and we will store the encrypted ciphertext in hexadecimal-encoded format - this can easily be stored in a text string.

Let's use the simplest mode of symmetric encryption, the Electronic Code Book (ECB) mode. This does not require any initialization vector and is probably the default mode in your encryption package. We do, however, need to pad our credit card input data to make it an exact multiple of the Triple DES block length of 8 bytes. In the examples that follow, we use the PKCS#5 method of padding.

Our first encryption operation in ECB mode is:

PT="1234-5678-9789-0124"
PT(hex)   =313233342D353637382D393738392D30313234
PT(padded)=313233342D353637382D393738392D303132340505050505
CT(hex)   =0BDC16E6A777C535C49F67688C6D4E21D3F36088C206C85A

Completing all the encryptions, our database table, with encrypted credit card number in hexadecimal (hex) format, becomes

ClientIDClientNameCreditCardNum
1John Doe0BDC16E6A777C535C49F67688C6D4E21D3F36088C206C85A
2Fred Bloggs0BDC16E6A777C535264AF5FD1E8BD570DDD44E842A72C00B
3Erika Mustermann5408551E9C4A0F8FC49F67688C6D4E21D3F36088C206C85A

OK, we have just used the FIPS-approved Triple DES encryption algorithm with a secret 192-bit key to encrypt our clients' credit card data. Couldn't be safer, could it? But, er, look at the results a bit closer here.

0BDC16E6A777C535C49F67688C6D4E21D3F36088C206C85A
0BDC16E6A777C535264AF5FD1E8BD570DDD44E842A72C00B
5408551E9C4A0F8FC49F67688C6D4E21D3F36088C206C85A

An encrypted form of a given input is meant to be indistinguishable from a random value. Do these results look random to you? No, they are not. Simply put, ECB mode is not secure - DO NOT USE IT!

Method 4

The best modes to use when encrypting are either Cipher Block Chaining (CBC) or Counter mode (CTR). These modes require that we use a unique Initialization Vector (IV) each time for a given key. Counter mode has the advantage that no padding is needed, but you need to be especially careful never to re-use an IV.

Let's add a new field to our database, IV, which will contain an 8-byte number randomly-generated each time the record changes. The chances of such an 8-byte IV value repeating with a proper random-number generator are infinitesimal. So now we'll generate a fresh, random 8-byte IV for each record, store this separately in the clear (which is OK), and produce a new set of encrypted credit card numbers using CBC mode.

ClientIDClientNameIVCreditCardNum
1John DoeF113709191C4259F8C2787E652FE8C37057E30A0139381FD4FE4635680E92324
2Fred Bloggs18DF733256D44E322874919B17EFEDFCCC0206723C26A003087D10A271449323
3Erika MustermannD23AB952DC37BFCA98185AC577517C1813CCC3FFF5C9FF86DD2E6A2D7A014DC3

This method is secure, providing you

  1. always use a new, freshly-randomly-generated IV every time you encrypt a new number or if you change someone's card number
  2. keep the key secret
  3. secure your system against unauthorised access

Method 5

So now your database administrator complains about having to add this new "IV" field. Well, why not join the IV value to the encrypted credit card number and store the concatenated value in the form

[IV] || [Encrypted credit card number]

So the same database can be stored like this

ClientIDClientNameCreditCardNum
1John DoeF113709191C4259F8C2787E652FE8C37057E30A0139381FD4FE4635680E92324
2Fred Bloggs18DF733256D44E322874919B17EFEDFCCC0206723C26A003087D10A271449323
3Erika MustermannD23AB952DC37BFCA98185AC577517C1813CCC3FFF5C9FF86DD2E6A2D7A014DC3

And when you need to decrypt the card number, you need to parse the value first to split off the IV. For a given block cipher algorithm, you always know exactly how long the IV will be. For Triple DES it is 8 bytes; for AES it is 16 bytes. So, taking the second example above, the concatenated encrypted value is

18DF733256D44E322874919B17EFEDFCCC0206723C26A003087D10A271449323
We split this into two parts, where the first 8 bytes is the IV
IV=18DF733256D44E32
CT=2874919B17EFEDFCCC0206723C26A003087D10A271449323
then we use the IV together with the secret key to decrypt the ciphertext and obtain
PT=313233342D353637312D393938382D373736360505050505
We need to strip off the padding, in this case, five bytes each of value 0x05 (this also provides a convenient check that the decryption was correct)
313233342D353637312D393938382D37373636
and then decode this from hex format into a string
1234-5671-9988-7766

Method 5a: Using base64

To shorten the length of the encrypted string before storing, we could use base64 encoding instead of hexadecimal.

ClientIDClientNameCreditCardNum
1John DoejCeH5lL+jDcFfjCgE5OB/U/kY1aA6SMk
2Fred BloggsKHSRmxfv7fzMAgZyPCagAwh9EKJxRJMj
3Erika MustermannmBhaxXdRfBgTzMP/9cn/ht0uai16AU3D

Method 5b: Using AES-128

This time we will repeat method 5 but use the AES-128 block cipher algorithm in counter (CTR) mode. We will use the 128-bit key
000102030405060708090A0B0C0D0E0F

Our input data is the same as before.

ClientIDClientNameCreditCardNum
1John Doe1234-5678-9789-0124
2Fred Bloggs1234-5671-9988-7766
3Erika Mustermann3456-7898-9789-0124

For AES we need to use a 16-byte IV, so we generate a new value for each record

ClientIDIV
17AD3C3BF888C9E88AA5F44773FAEB42E
24CC0646AACFAAF8D9E2B3D7BEC1E1232
3A9043A577688BD354FCD6CDC85DBD7CB

To encrypt the first record, we have

KEY=000102030405060708090A0B0C0D0E0F
IV =7AD3C3BF888C9E88AA5F44773FAEB42E
PT="1234-5678-9789-0124"
PT(hex)   =313233342D353637382D393738392D30313234
CT(hex)   =612B4B355C9874F920AC346BD0F8C5C7614165

Note that padding is not needed with CTR mode and the output ciphertext is the same length as the input. This has the advantage that there is no padding to remove when decrypting and less data to store, but you lose the check for correct decryption provided by CBC mode with PKCS#5 padding.

Repeating this procedure gives the following ciphertext values

PlaintextCiphertext
1234-5678-9789-0124612B4B355C9874F920AC346BD0F8C5C7614165
1234-5671-9988-7766D51615107F3B298596E9170083EE0F093E0C69
3456-7898-9789-012424C2FE415F3C160AC7106114C9E0DAC31FCA60

The resulting table is as follows:

ClientIDClientNameCreditCardNum
1John Doe7AD3C3BF888C9E88AA5F44773FAEB42E612B4B355C9874F920AC346BD0F8C5C7614165
2Fred Bloggs4CC0646AACFAAF8D9E2B3D7BEC1E1232D51615107F3B298596E9170083EE0F093E0C69
3Erika MustermannA9043A577688BD354FCD6CDC85DBD7CB24C2FE415F3C160AC7106114C9E0DAC31FCA60

Method 6

The concatenated encrypted field is getting a bit long. The 16-byte IV required for AES is almost as long as the data. How can we shorten it? Here is a refinement where we use the record ID as part of the IV.

In a database with an automatically-generated ID, each new record is guaranteed to have a unique ID value, so you could, in principle, use just the ID number as the IV, suitably padded to the required block length.

ClientIDIV
10000000000000001
20000000000000002
30000000000000003

The problem with this is that, if a client changes their credit card number, then you are using the same IV to encrypt a different value - DO NOT DO THIS!.

A more secure approach is to randomly generate, say, the first eight bytes and then append the ID number to make the required 16-byte IV.

ClientIDRandomIV
17AD3C3BF888C9E887AD3C3BF888C9E880000000000000001
24CC0646AACFAAF8D4CC0646AACFAAF8D0000000000000002
3A9043A577688BD35A9043A577688BD350000000000000003

So you only need to store the 8-byte random component instead of full 16-byte value. The chances of getting the same IV is still impossible, provided your random-number generator is OK. Appending the unique record ID makes it even less likely you will get the same value. Don't worry about all the zero padding bytes in the IV values here; an IV just has to be unique. In this case, you save 16 hex characters in the output string, with the minor downside that you now need to reconstruct the full IV when decrypting. (You could go even shorter and just generate and store, say, 4 random bytes - the odds of getting the same 32-bit value are still 4 billion to one. But don't go any shorter than 4 bytes.)

Using this method, the table would be

ClientIDClientNameCreditCardNum
1John Doe7AD3C3BF888C9E885CC206D5822ABE44D665F8A47CCB1A02FF069A
2Fred Bloggs4CC0646AACFAAF8D68CAACCACE1D3EE96D7DC7EB7C0F936E89829B
3Erika MustermannA9043A577688BD35BD99442B4265B7E8E0AF9E3B700665E37A1CCC

To decrypt, say, the third example above, we read the fields from the database

ID=3
CreditCardNum=A9043A577688BD35BD99442B4265B7E8E0AF9E3B700665E37A1CCC
We construct the IV as follows
IV = [random 8 bytes] || [ID padded to 8 bytes] = A9043A577688BD350000000000000003
and the ciphertext is
CT=BD99442B4265B7E8E0AF9E3B700665E37A1CCC
so the input to the decrypt function is
KEY=000102030405060708090A0B0C0D0E0F
IV =A9043A577688BD350000000000000003
CT =BD99442B4265B7E8E0AF9E3B700665E37A1CCC
producing the output in hex
PT =313233342D353637382D393738392D30313234
which when decoded to ASCII text gives the credit card number:
3456-7898-9789-0124

Summary

 

Contact us

Any comments, feedback, questions to our email or use our Contact Page.

This page last updated: 26 May 2008


Valid XHTML 1.0! Copyright © 2008 D.I. Management Services Pty Limited ABN 78 083 210 584, Sydney, Australia. All rights reserved.
<www.di-mgt.com.au>    <Cryptography Page>