DEV Community

Isaac Lyman
Isaac Lyman

Posted on • Originally published at isaaclyman.com

Credit card chips and encryption in plain JavaScript

Your current credit card can be used in four different ways:

  1. Typing in the card number. We've been using credit cards this way since 1950.
  2. Sliding the card's magnetic strip (the black stripe) through a card reader. This method was invented by IBM in the 1960s and has been in use ever since, but it's on its way out. In a few short years many credit cards will no longer have a magnetic strip. With luck, we'll phase them out completely sometime in the 2030s.
  3. Inserting the card into a chip reader. Chip cards were first invented in the 1960s, believe it or not. France and Germany developed standards for them in the 1980s, but it took until the 2000s for a worldwide standard (EMV) to catch on, and the U.S. didn't start issuing chip cards until the mid-2010s. By now, every major credit card has a microchip in it.
  4. Waving the card over a contactless chip reader. This is essentially the same as inserting the card and uses the same embedded microchip.

Today I'll be going in-depth on these last two, explaining what the "chip" on your card is and how it works. The technology involved is closely related to the tech that keeps web browsing and app downloads secure.

The microchip

You've no doubt noticed the shiny metal chip on the front of your credit card. Except that's not a chip; it's an electrical contact. When you insert your card, electricity is supplied through the contact's top left pin to power the actual microchip, which is embedded inside the card, and other pins are used to communicate between the microchip and the payment terminal. If your card is "tap to pay" enabled, it also has an antenna around the inside edge that receives wireless power and communicates with the card reader via radio waves.

To be clear, the microchip on your credit card is a computer! No, really. It's a very small computer that runs one or more applications. So that's cool. You're probably carrying two or three computers in your wallet right now.

Why does your credit card need an onboard computer? Isn't that going overboard?

Think about how easy it is to steal someone's credit card information right now. A quick photo will do it. The card's "passwords"—a name, 16-digit number, expiration date, and security code—are all clearly displayed. Many of us have had the unpleasant experience of discovering someone else is using our credit card number and we don't even know how they got it. In fact, there's a thriving industry around credit card fraud: companies that buy and sell stolen numbers, other companies that test them to see if they still work, and tens of billions of dollars circulating between them and an army of online and offline thieves.

Even if the card were blank except for the magnetic strip, it would still be a relatively easy target. A credit card's number never changes. And reading magnetic strips is decades-old technology, so simple it can be done by a device even smaller than a credit card (including onboard memory). Devices like these (called "skimmers") are commonly inserted in gas station card readers and left to collect data for a couple days before being retrieved. So neither credit card numbers nor magnetic strips are up to the task of protecting your financial identity.

This is a problem well worth solving. But credit cards need to identify themselves somehow. The card reader at the grocery store has to tell your bank how much you're spending, and the bank needs to deduct money from your account. If a hacker is listening in on that electronic conversation, how can we stop them from learning enough to use your card for fraud?

That's very similar to another topic I've written about: how HTTPS stops hackers from reading the information you send over the Internet. When you visit a website, your browser and the website establish a secret decoding key together, in the open, and nobody else can figure out what it is even if they're listening the whole time. That's the magic of prime numbers.

So is your credit card doing an HTTPS key exchange with the bank? Well, not quite.

Encryption and symmetry

If you've ever written to a friend in a made-up language or used a numeric code (A=1, B=2, C=3...) you understand the concept of encryption. Encryption is taking a message and disguising it with some kind of algorithm; decryption is reversing the algorithm to decode it and see the original message. Nowadays we encrypt nearly everything online. We might as well; it's fast and free, and it's always better to err on the side of privacy.

Sometimes we need to encrypt something so hard it can never be decrypted. We do this for passwords (or try to). There's absolutely no reason for anyone but you to know your password. The websites you log into store an irreversibly encrypted hash of your password, then use that hash for comparison every time you log in. They don't know your password, but they can check to see if you're typing the right one.

This isn't one of those times. Your credit card needs to supply a piece of information that the bank (but nobody else) can quickly decrypt.

One way to do this is by using symmetric key encryption (also called private key encryption). This requires the credit card and the bank to have a shared, predetermined key: a string of bytes that can be used to encrypt or decrypt any message. That's no problem, since the card came from the bank in the first place; they can program the key into the card before they mail it to you.

Let's implement a two-round symmetric key encryption algorithm in JavaScript. It will be far too simple and insecure to use in real life, but it'll illustrate how an algorithm like AES works. We'll be using the bitwise XOR operator ^, which you probably don't see very often, but all you need to know is if a ^ b === c, then c ^ a === b and c ^ b === a.

//
// ENCRYPTION
//

// This is the entry point. It converts your shared key to a number
//  and your message to an array of numbers, then encrypts it in two
//  steps. It converts the resulting array of numbers back to a string
//  and returns it.
function encrypt(sharedKey, message) {
  const numericKey = stringKeyToNumeric(sharedKey)
  const messageBytes = stringToIntArray(message)
  const r1 = encryptRound1(numericKey, messageBytes)
  const r2 = encryptRound2(numericKey, r1)
  const encrypted = intArrayToString(r2)
  return encrypted
}

function encryptRound1(numericKey, byteArray) {
  // Create a "round key" so we aren't using the same key for every step
  const roundKey = numericKey ^ 4321

  // For each character in the message, XOR it with the round key
  return byteArray.map(byte => byte ^ roundKey)
}

function encryptRound2(numericKey, byteArray) {
  const roundKey = numericKey % 1234
  const newBytes = byteArray.slice()

  // "Rotate" the array (move the last element to the beginning) roundKey times
  for (let step = 0; step < roundKey; step++) {
    newBytes.unshift(newBytes.pop())
  }
  return newBytes
}

//
// DECRYPTION
//

// The other entry point. It does the same thing as `encrypt` but backwards.
function decrypt(sharedKey, encrypted) {
  const numericKey = stringKeyToNumeric(sharedKey)
  const messageBytes = stringToIntArray(encrypted)
  const r1 = decryptRound1(numericKey, messageBytes)
  const r2 = decryptRound2(numericKey, r1)
  const message = intArrayToString(r2)
  return message
}

function decryptRound1(numericKey, byteArray) {
  const roundKey = numericKey % 1234
  const newBytes = byteArray.slice()

  // "Unrotate" the array roundKey times
  for (let step = 0; step < roundKey; step++) {
    newBytes.push(newBytes.shift())
  }
  return newBytes
}

function decryptRound2(numericKey, byteArray) {
  const roundKey = numericKey ^ 4321

  // XOR each character with the same round key, which gives us the original
  return byteArray.map(byte => byte ^ roundKey)
}

//
// UTILITY
//

function stringToIntArray(message) {
  return message.split('').map(char => char.charCodeAt(0))
}

function intArrayToString(intArray) {
  return intArray.map(int => String.fromCharCode(int)).join('')
}

function stringKeyToNumeric(key) {
  const fourDigitCodes = key.split('').map(char => char.charCodeAt(0).toString().padStart(4, '0'))
  const numericKey = Number(fourDigitCodes.join(''))
  return numericKey
}

//
// TEST PROGRAM
//

const message = "Hello there."
const sharedKey = "ABCDEF"

console.log('Message:', message) // => Message: Hello there.

const encrypted = encrypt(sharedKey, message)
console.log('Encrypted:', encrypted) // => Encrypted: ႎჁ႕ႉႄ႓ႄ჏Ⴉႄႍႍ

const decrypted = decrypt(sharedKey, encrypted)
console.log('Decrypted:', decrypted) // => Decrypted: Hello there.
Enter fullscreen mode Exit fullscreen mode

Go ahead, paste this into a REPL and play with it if you'd like.

Some of you might be saying "this is what you call simple?!" And others might be saying "this is way too simple, where are the lookup tables and block ciphers?" Look, I can't please everyone but I can upset everyone, so that's what I went with.

Let's put this code in context. If my credit card's microchip has the shared key "ABCDEF" programmed into it and my bank knows to use that key for messages that come from me, here's a simplified example of how a purchase authorization might go:

Payment terminal: I'm verifying a purchase of four chinchillas at Pet Central for 89.99 USD at 11:15 PM, Oct 10 2022. Please respond.

Microchip: Okay. I've received the transaction but I'll need a PIN number to authorize it.

Payment terminal: Sure thing. Customer, please enter your PIN.

Me: *Types 9999.*

Payment terminal: Is that the right PIN, microchip?

Microchip: Yep, that's the one. I trust this transaction. encrypt("ABCDEF", "Pet Central: 89.99 USD at 11:15 PM, Oct 10 2022")...done. The transaction has been encrypted with my private key. Please send it to FIRST BANK and tell them it's from ISAAC LYMAN, whose account number is 12345678.

Payment terminal: Great. Contacting the bank...

FIRST BANK: Hello, FIRST BANK speaking.

Payment terminal: I'm a payment terminal at Pet Central. I want to process a transaction for account 12345678 under the name ISAAC LYMAN.

FIRST BANK: Transaction received. decrypt("ABCDEF", transaction)...and done. I can verify this transaction is from ISAAC LYMAN's credit card because otherwise it wouldn't decrypt. Everything lines up, so I'll deduct 89.99 USD from his account and send it over.

Payment terminal: Transaction successful.

Note that my credit card number doesn't have to be sent over the wire. Neither does my private key. In fact, nothing worth stealing is leaving the terminal. A hacker could grab the encrypted transaction message, but there's no way to use it unless they have a time machine and want to buy four chinchillas from Pet Central at the exact same time I originally bought them.

Symmetric key encryption is used by some banks, but it has a couple of minor flaws. One of them is that theoretically, the bank could make up a fake transaction and charge you for it. They've got your private key, so the only thing stopping them is hundreds of international regulations. The other flaw is that your private key is stored on an Internet-connected server at the bank, so it could be stolen if there's a security breach.

To be clear: symmetric key encryption is so much better than credit card numbers and magnetic strips. But there's an even better way.

Asymmetric keys

Symmetric keys make a lot of sense. Of course if you and another person have a shared secret key, you can encrypt and decrypt each other's messages.

Asymmetric keys step into the realm of magic. Or, at least, counterintuitive math.

In asymmetric encryption, there are two different keys: a private key and a public key. These keys are mathematically related, but knowing one won't help you figure out the other. They serve opposite functions: you can encrypt a message with the private key that can only be decrypted by the public key, and you can encrypt a message with the public key that can only be decrypted by the private key.

Asymmetric encryption is often used by journalists for sensitive investigations. They can generate a key pair, store the private key on a device they trust, and post the public key for the whole world to see. Anyone who wants to send them a secret message can use the public key to encrypt it, ensuring only the journalist will be able to decrypt it. And in the opposite direction, the journalist can encrypt a message using their private key (or encrypt a hash of it, which we call "signing" it), which provides proof of authorship. If the message (or hash) can be decrypted using their public key, that proves it came from them.

Back to credit cards. The bank can program a public/private key pair onto your credit card and then completely forget the private key. They don't need to store it anywhere. They only need to hold onto the public key. They'll use it for two purposes: to decrypt messages sent from your card and to prove those messages came from you, not them.

This eliminates the first risk of symmetric keys: fraud committed by the bank itself. As long as they can prove they didn't keep your private key after they programmed your card, they can't be accused of generating fake transactions. It also eliminates the second risk. Your private key isn't stored on any Internet-connected server; it only lives in one place in the entire world, and that's inside your credit card. Sure, your public key could be stolen. But the only thing the thieves could do with it is read your transactions. They wouldn't be able to create new ones.

Let's implement some asymmetric encryption in JavaScript. This will illustrate how an algorithm like RSA works.

//
// ENCRYPTION
//

function encrypt(privateKey, message) {
  const messageBytes = stringToIntArray(message)
  const [modulus, exponent] = privateKey
  const encrypted = messageBytes.map(byte => (BigInt(byte) ** exponent) % modulus)
  return intArrayToString(encrypted)
}

//
// DECRYPTION
//

function decrypt(publicKey, encrypted) {
  const encryptedBytes = stringToIntArray(encrypted)
  const [modulus, exponent] = publicKey
  const decrypted = encryptedBytes.map(byte => (BigInt(byte) ** exponent) % modulus)
  return intArrayToString(decrypted)
}

//
// UTILITY
//

function stringToIntArray(message) {
  return message.split('').map(char => char.charCodeAt(0))
}

function intArrayToString(intArray) {
  return intArray.map(int => String.fromCharCode(Number(int))).join('')
}

//
// TEST PROGRAM
//

/* 
  Each key has two parts: a shared "modulus" and a non-shared "exponent."
  Key generation is complex. These three numbers all have to be mathematically related.
  In real life the modulus would be a very large number in the ballpark of
  5353683424958100487553692737855921351167231508809347122534824314767512079033611122618
  8508861310385963124739442570162267402922655660049366578306762217485423974277904708682
  5660398049299993503929195152693455928734884540497474885533510411221183157745113097158
  7147857841470416503261588799943431828622126181414376019549024568602030872555448200000
  2641304272163777569374865564566594216223640291860534772832785404395599963806669068407
  0321707428203752195331410456415056194233539898345799623002726720980709094812380346346
  3583607751796240362096547051130726215056560026642532836575212569426651466587428695230
  1071376112504419090641365.
  The exponents would be somewhat smaller. Both exponents must be prime numbers.

  We use the `n` suffix to get BigInts, since playing with exponents can exceed
  JavaScript's standard number limit pretty quickly.
*/
const privateKey = [25777n, 3n]
const publicKey = [25777n, 16971n]

const message = "Four chinchillas"
console.log("Message:", message) // => Message: Four chinchillas

const encrypted = encrypt(privateKey, message)
console.log("Encrypted:", encrypted) // => Encrypted: ớ֪൯⿟᭏䂦䁅宍㿵䂦䁅宍垐垐⣮ 

const decrypted = decrypt(publicKey, encrypted)
console.log("Decrypted:", decrypted) // => Decrypted: Four chinchillas
Enter fullscreen mode Exit fullscreen mode

A few things you might be thinking at this point:

  • Wait, this is shorter than the other code snippet. If that doesn't seem right, it's because it isn't. This is only the asymmetric part. In real life you'd do some reversible encryption ("padding") rounds before you use your asymmetric key so the encrypted text couldn't be cracked by a letter-frequency attack. But that's not the whole story either. Typically a message sender asymmetrically encrypts a randomly-generated key, uses that key to symmetrically encrypt the message, and sends the symmetrically encrypted message and asymmetrically encrypted key together. (This is because symmetric encryption is faster.)
  • Hey, this looks a lot like a Diffie-Hellman key exchange! Well, that's because in both cases we're looking for a computation that's easy to do but hard to reverse, and we're looking for a way to encrypt and decrypt a message using numeric counterparts. Prime numbers are easy to multiply and the result is hard to factor—that's part one. And part two: exponents and moduluses have mystical properties that allow numbers to travel between dimensions unharmed.
  • Good grief, this is complicated. And that's why you should never do it yourself. Make even one tiny mistake and your security goes down the drain. You should always use a popular, well-vetted cryptography library in whatever language you're programming in. This bears repeating: never implement an encryption algorithm on your own.

Some banks use asymmetric key pairs in credit card chips. This creates an opportunity for a few extra security features. For example, if the bank has their own public/private key pair, they can publish their public key and use their private key to sign your public key, which creates a chain of evidence linking your card to them. When you use your card, it can send its public key to the terminal with a cryptographic signature from the bank proving they issued it! The payment terminal just needs to know the bank's public key to verify the signature. Then when the card signs a transaction, the terminal can check that it matches the signed public key. This allows offline, computational assurance that a card is authentic.

Unfortunately, complete security is impossible. A computer is only secure until it gets into the wrong hands. Your card's microchip is engineered not to reveal its private key, but all bets are off when it comes to physical access. So if you lose your wallet, you still need to call your bank.

The future of credit cards

Many stores are reluctant to upgrade their payment terminals—it's a hard cost with fuzzy benefits—but most of them did so in 2015, when card issuers updated their agreements to make stores liable for credit card fraud if their terminals didn't have EMV chip readers. Liability is a powerful force in the business world. So now most of the places you shop let you insert your card's microchip to pay, and quite a few also have contactless tap-to-pay terminals.

What's next?

It would be great to get rid of visible credit card numbers and magnetic strips. The credit card fraud industry would be left gasping for air. One major holdout is online shopping. You still have to type in your name, card number, expiration date, and CVV code to buy something from a website. But the world is ripe for change; most smartphones made in the last five years have NFC chips, and roughly half of all e-commerce happens on smartphones. So with a little work on the software side, it's not hard to imagine a world where you could pay for something by waving your credit card over your phone screen.

Then again, that may be too many steps. Both iPhone and Android have mobile wallet functionality so you can program your cards into your phone and use your phone to pay both in-person (at tap-to-pay terminals) and at some online stores. This doesn't need to go away; perhaps there's a version of the future where you add cards to your mobile wallet using a direct connection to the issuing bank instead of a few easily-stolen strings of digits.

The ideal credit card would be one that couldn't be used fraudulently unless it was physically stolen by someone with advanced technology and plenty of time on their hands. You couldn't reveal its secrets to a scam caller even if you wanted to.

Like so many other things, we'll leave it in the capable hands of a microchip.

Acknowledgments

Thanks to the following authors, without whose work I wouldn't understand this subject at all:

Top comments (0)