Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

updated to use https://github.com/ubiqsecurity/ubiq-fpe-go #6

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.20

- name: Build
run: go build -v ./...
Expand Down
19 changes: 12 additions & 7 deletions ff3Token.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ package ff3Token
import (
"errors"

"github.com/capitalone/fpe/ff3"
fpe "gitlab.com/ubiqsecurity/ubiq-fpe-go"
)

// this is a wrapper for the ff3 Cipher
type Cipher struct {
ff3Cipher ff3.Cipher
cipher *fpe.FF3_1
tweak []byte
}

// NewCipher initializes a new FF3 Token Cipher for encryption or decryption use key and tweak parameters.
const radixLength = 52

const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP"

// NewCipher initializes a new FF3-1 Token Cipher for encryption or decryption use key and tweak parameters.
// Radix is not exposed, since for this algorithm it must be 52 [a-zA-Z]
func NewCipher(key []byte, tweak []byte) (Cipher, error) {
cipher, err := ff3.NewCipher(52, key, tweak)
return Cipher{ff3Cipher: cipher}, err
cipher, err := fpe.NewFF3_1(key, tweak, radixLength, alphabet)
return Cipher{cipher: cipher, tweak: tweak}, err
}

// Encrypt is a wrapper around ff3.Encrypt, input must be Numeric
Expand All @@ -25,7 +30,7 @@ func (c Cipher) Encrypt(X string) (string, error) {
return "", errors.New("invalid input sent to Encrypt (must be numeric)")
}

result, err := c.ff3Cipher.Encrypt(X)
result, err := c.cipher.Encrypt(X, c.tweak)
if err != nil {
return "", err
}
Expand All @@ -40,7 +45,7 @@ func (c Cipher) Decrypt(X string) (string, error) {
if err != nil {
return newX, err
}
decrypted, err := c.ff3Cipher.Decrypt(newX)
decrypted, err := c.cipher.Decrypt(newX, c.tweak)
if err != nil {
return "", err
}
Expand Down
66 changes: 33 additions & 33 deletions ff3Token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package ff3Token
import (
"encoding/hex"
"fmt"
_ "net/http/pprof"
"testing"
)

Expand All @@ -25,21 +26,21 @@ var testVectors = []testVector{
// this simulates multiple environments that can have completely different tokens even if they encrypted the exact same data.
{
"EF4359D8D580AA4F7F036D6F04FC6A94",
"D8E7920AFA330A73",
"D8E7920AFA330A",
"4147000000001234", // simulated Visa card
"WIrhsWqFLLbFPWpb",
"yinTttUBsMhDMLPh",
"", "",
},
{
"EF4359D8D580AA4F7F036D6F04FC6A93",
"D8E7920AFA330A73",
"D8E7920AFA330A",
"4147000000001234", // simulated Visa card
"IjKelwlRMiqljyYq",
"uLEzwJuxwTSpsNlE",
"", "",
},
{
"EF4359D8D580AA4F7F036D6F04FC6A93",
"D8E7920AFA330A73",
"D8E7920AFA330A",
"414700000000123x", // invalid input data this "CC number" has a letter in it
"IjKelwlRMiqljyYx",
"invalid input sent to Encrypt (must be numeric)",
Expand All @@ -48,35 +49,35 @@ var testVectors = []testVector{
// test empty string (actual error comes from the FF3 lib, ff3token in theory shouldn't care, )
{
"EF4359D8D580AA4F7F036D6F04FC6A93",
"D8E7920AFA330A73",
"D8E7920AFA330A",
"",
"",
"message length is not within min and max bounds",
"message length is not within min and max bounds",
"invalid text length",
"invalid text length",
},
{
"",
"",
"",
"",
"key length must be 128, 192, or 256 bits",
"key length must be 128, 192, or 256 bits",
"invalid tweak length",
"invalid tweak length",
},
{
"EF4359D8D580AA4F7F036D6F04FC6A93",
"",
"",
"",
"tweak must be 8 bytes, or 64 bits",
"tweak must be 8 bytes, or 64 bits",
"invalid tweak length",
"invalid tweak length",
},
{
"EF4359D8D580AA4F7F036D6F04FC6A94",
"D8E7920AFA330A73",
"D8E7920AFA330A",
"4",
"W",
"message length is not within min and max bounds",
"message length is not within min and max bounds",
"invalid text length",
"invalid text length",
},
}

Expand Down Expand Up @@ -174,7 +175,7 @@ func ExampleCipher_Encrypt() {
if err != nil {
panic(err)
}
tweak, err := hex.DecodeString("D8E7920AFA330A73")
tweak, err := hex.DecodeString("D8E7920AFA330A")
if err != nil {
panic(err)
}
Expand All @@ -194,7 +195,7 @@ func ExampleCipher_Encrypt() {
}

fmt.Println(ciphertext)
// Output: OOGkpxFEKMmCufxYul
// Output: YgzAwpwEZRxYQvZiEW
}

// Note: panic(err) is just used for example purposes.
Expand All @@ -205,7 +206,7 @@ func ExampleCipher_Decrypt() {
if err != nil {
panic(err)
}
tweak, err := hex.DecodeString("D8E7920AFA330A73")
tweak, err := hex.DecodeString("D8E7920AFA330A")
if err != nil {
panic(err)
}
Expand All @@ -216,7 +217,7 @@ func ExampleCipher_Decrypt() {
panic(err)
}

ciphertext := "OOGkpxFEKMmCufxYul"
ciphertext := "YgzAwpwEZRxYQvZiEW"

plaintext, err := FF3.Decrypt(ciphertext)
if err != nil {
Expand All @@ -230,24 +231,23 @@ func ExampleCipher_Decrypt() {
func BenchmarkEncrypt(b *testing.B) {
for idx, testVector := range testVectors {
sampleNumber := idx + 1
b.Run(fmt.Sprintf("Sample%d", sampleNumber), func(b *testing.B) {
key, err := hex.DecodeString(testVector.key)
if err != nil {
b.Fatalf("Unable to decode hex key: %v", testVector.key)
}

tweak, err := hex.DecodeString(testVector.tweak)
if err != nil {
b.Fatalf("Unable to decode tweak: %v", testVector.tweak)
}
key, err := hex.DecodeString(testVector.key)
if err != nil {
b.Fatalf("Unable to decode hex key: %v", testVector.key)
}

ff3, err := NewCipher(key, tweak)
if err != nil {
b.Fatalf("Unable to create cipher: %v", err)
}
tweak, err := hex.DecodeString(testVector.tweak)
if err != nil {
b.Fatalf("Unable to decode tweak: %v", testVector.tweak)
}

b.ResetTimer()
ff3, err := NewCipher(key, tweak)
if err != nil {
b.Fatalf("Unable to create cipher: %v", err)
}

b.Run(fmt.Sprintf("Sample%d", sampleNumber), func(b *testing.B) {
for n := 0; n < b.N; n++ {
ff3.Encrypt(testVector.plaintext)
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/bdw666/ff3Token

go 1.18
go 1.20

require github.com/capitalone/fpe v1.2.1
require gitlab.com/ubiqsecurity/ubiq-fpe-go v0.0.0-20230601140135-04d38c981c56
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/capitalone/fpe v1.2.1 h1:/r81KhhTkfmxjjr2HKr+WYTLrMjPnn0gtK/L8gKNfts=
github.com/capitalone/fpe v1.2.1/go.mod h1:hI6YzL2v2WkosaevH24sYHyyDAzacfqkpaOYc/0Qn7g=
gitlab.com/ubiqsecurity/ubiq-fpe-go v0.0.0-20230601140135-04d38c981c56 h1:U53/cS+eqVL7aAqzVQgl0Jx3DAwUqf8RpTytJ9RVluU=
gitlab.com/ubiqsecurity/ubiq-fpe-go v0.0.0-20230601140135-04d38c981c56/go.mod h1:qlLxh57bIu15/S4B9pP1FO7niJzQKr5h17fetuUEuh8=
8 changes: 4 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
![GitHub CI](https://github.com/bdw666/ff3Token/actions/workflows/go.yaml/badge.svg)
[![Go Reference](https://pkg.go.dev/badge/github.com/bdw666/ff3Token.svg)](https://pkg.go.dev/github.com/bdw666/ff3Token)
![GitHub CI](https://github.com617/ff3Token/actions/workflows/go.yaml/badge.svg)
[![Go Reference](https://pkg.go.dev/badge/github.com/bdw617/ff3Token.svg)](https://pkg.go.dev/github.com/bdw617/ff3Token)

# FF3Token

This is a thin layer on top of https://github.com/capitalone/fpe to make encrypted data and decrypted data not share the same dictionary so it's obvious which is encrypted and what is decrypted data. Since FF3 will produce perfectly fine looking decrypted data this provides a layer. The original design for this was done for PCI compliance to minimize the impact of the rest of the system in scope for a PCI audit. (Credit card numbers were the primary use cases, but it can be used for social security numbers, account numbers, or any other data you need to encrypt in place)
This was a thin layer on top of https://github.com/capitalone/fpe, but to use FF3-1, it's now using: https://gitlab.com/ubiqsecurity/ubiq-fpe-go to make encrypted data and decrypted data not share the same dictionary so it's obvious which is encrypted and what is decrypted data. Since FF3 will produce perfectly fine looking decrypted data this provides a layer. The original design for this was done for PCI compliance to minimize the impact of the rest of the system in scope for a PCI audit. (Credit card numbers were the primary use cases, but it can be used for social security numbers, account numbers, or any other data you need to encrypt in place)

## Disclaimer
This is NOT general purpose cryptography, the data to be encrypted with this algorithm is one of many things good engineering should do to properly secure user data. I'm not a cryptographer and have not performed signifiicant crytoanalysis on the algorithm. If you choose to follow this pattern, that's exciting, please let me know! Please do it with the proper experts to review your design.

## note on FF3
FF3-1 would have been the prefered algorithm but there's no open source in Golang I can find, Hashicorp vault has implemented it as part of their Transform engine, but it's expensive. For a commercial implementation of tokenization, I do recommend having the crypt be in software that meets all your compliance requirements. Then this code is just application code!
The old capitalone library never supported FF3-1, but the UBIQ one does, so this code was migrated to use the UBIQ one.

## What is this?
This is a simple layer on top of CapitolOne's FF3 algorithm built in Golang. It Enforces input data for Encryption to be numeric, and validates the Decrypted data is numeric. The advantage of this is users and systems can now identify whether or not something is a token (all letters), versus a credit card number (all numbers). Since this is format preserving encryption, the encrypted token fits in the same space as the input data. This allows you to keep the input data as close to source of truth as possible (especially when dealing with fixed with data formats).
Expand Down