diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index f5fdaf0..081e576 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -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 ./... diff --git a/ff3Token.go b/ff3Token.go index 39ae243..6201770 100644 --- a/ff3Token.go +++ b/ff3Token.go @@ -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 @@ -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 } @@ -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 } diff --git a/ff3Token_test.go b/ff3Token_test.go index 7ad4676..8be48ae 100644 --- a/ff3Token_test.go +++ b/ff3Token_test.go @@ -6,6 +6,7 @@ package ff3Token import ( "encoding/hex" "fmt" + _ "net/http/pprof" "testing" ) @@ -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)", @@ -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", }, } @@ -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) } @@ -194,7 +195,7 @@ func ExampleCipher_Encrypt() { } fmt.Println(ciphertext) - // Output: OOGkpxFEKMmCufxYul + // Output: YgzAwpwEZRxYQvZiEW } // Note: panic(err) is just used for example purposes. @@ -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) } @@ -216,7 +217,7 @@ func ExampleCipher_Decrypt() { panic(err) } - ciphertext := "OOGkpxFEKMmCufxYul" + ciphertext := "YgzAwpwEZRxYQvZiEW" plaintext, err := FF3.Decrypt(ciphertext) if err != nil { @@ -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) } diff --git a/go.mod b/go.mod index 8f55629..a79ae96 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 993d652..a3277c3 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/readme.md b/readme.md index c7b6418..334ab2e 100644 --- a/readme.md +++ b/readme.md @@ -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).