├── .github
└── workflows
│ └── build-gradle.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── settings.gradle.kts
└── src
├── jmh
└── java
│ └── com
│ └── privacylogistics
│ └── FF3CipherPerf.java
├── main
└── java
│ └── com
│ └── privacylogistics
│ └── FF3Cipher.java
└── test
└── java
└── com
└── privacylogistics
└── FF3CipherTest.java
/.github/workflows/build-gradle.yml:
--------------------------------------------------------------------------------
1 | name: Java CI
2 |
3 | on:
4 | push:
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout project sources
12 | uses: actions/checkout@v4
13 | - name: Setup Gradle
14 | uses: gradle/actions/setup-gradle@v4
15 | - name: Run build
16 | run: gradle build
17 |
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .*
2 | !.github/
3 | !.gitignore
4 | !.travis.yml
5 |
6 | # Distribution / packaging
7 | build/
8 | gradle/
9 | gradlew
10 | gradlew.bat
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/mysto/java-fpe/actions)
2 | [](https://opensource.org/licenses/Apache-2.0)
3 | [](https://maven-badges.herokuapp.com/maven-central/io.github.mysto/ff3)
4 | [](https://javadoc.io/doc/io.github.mysto/ff3)
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 | # ff3 - Format Preserving Encryption in Java
16 |
17 | An implementation of the NIST approved FF3 and FF3-1 Format Preserving Encryption (FPE) algorithms in Java.
18 |
19 | This package follows the FF3 algorithm for Format Preserving Encryption as described in the March 2016 NIST publication 800-38G _Methods for Format-Preserving Encryption_,
20 | and revised on February 28th, 2019 with a draft update for FF3-1.
21 |
22 | * [NIST Recommendation SP 800-38G (FF3)](http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf)
23 | * [NIST Recommendation SP 800-38G Revision 1 (FF3-1)](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38Gr1-draft.pdf)
24 | * [NIST SP 800-38G Revision 1 (2nd Public Draft)](https://csrc.nist.gov/pubs/sp/800/38/g/r1/2pd)
25 |
26 | **NOTE:** NIST's Feburary 2025 Draft 2 has removed FF3 from the NIST standard. Contact me about a licensed version of FF1 in Java.
27 |
28 | Changes to minimum domain size and revised tweak length have been implemented in this package with
29 | both 64-bit and 56-bit tweaks are supported. NIST has only published official test vectors for 64-bit tweaks, but draft ACVP test vectors have been used for testing FF3-1. It is expected the final
30 | NIST standard will provide updated test vectors with 56-bit tweak lengths.
31 |
32 | ## Use
33 |
34 | To use the package, you need to use following Maven dependency:
35 |
36 | ```maven
37 |
38 | io.github.mysto
39 | ff3
40 | 1.0
41 |
42 | ```
43 | or Gradle Kotlin:
44 |
45 | ```gradle
46 | implementation("io.github.mysto:ff3:1.0")
47 | ```
48 | or simply download jars from the Maven Central repository.
49 |
50 | This package has external dependencies only on Log4j and testing (which uses JUnit).
51 |
52 | ## Usage
53 |
54 | FF3 is a Feistel cipher, and Feistel ciphers are initialized with a radix representing an alphabet. The number of
55 | characters in an alphabet is called the _radix_. The following radix values are common:
56 | * radix 10: digits 0..9
57 | * radix 36: alphanumeric 0..9, a-z
58 | * radix 62: alphanumeric 0..9, a-z, A-Z
59 |
60 | Special characters and international character sets, such as those found in UTF-8, would require a larger radix, and are not supported.
61 | Also, all elements in a plaintext string share the same radix. Thus, an identification number that consists of a letter followed
62 | by 6 digits (e.g. A123456) cannot be correctly encrypted by FPE while preserving this convention.
63 |
64 | Input plaintext has maximum length restrictions based upon the chosen radix (2 * floor(96/log2(radix))):
65 | * radix 10: 56
66 | * radix 36: 36
67 | * radix 62: 32
68 |
69 | To work around string length, its possible to encode longer text in chunks.
70 |
71 | The key length must be 128, 192, or 256 bits in length. The tweak is 7 bytes (FF3-1) or 8 bytes for the origingal FF3.
72 |
73 | As with any cryptographic package, managing and protecting the key(s) is crucial. The tweak is generally not kept secret.
74 | This package does not store the key in memory after initializing the cipher.
75 |
76 | ## Code Example
77 |
78 | The example code below can help you get started.
79 |
80 | Using default domain [0-9]
81 |
82 | ```jshell
83 | jshell --class-path build/libs/ff3-X.X.jar:~/lib/log4j-core-2.24.3.jar:~/lib/log4j-api-2.24.3.jar
84 |
85 | import com.privacylogistics.FF3Cipher;
86 | FF3Cipher c = new FF3Cipher("2DE79D232DF5585D68CE47882AE256D6", "CBD09280979564");
87 | String pt = "3992520240";
88 | String ciphertext = c.encrypt(pt);
89 | String plaintext = c.decrypt(ciphertext);
90 | pt;ciphertext;plaintext
91 | ```
92 |
93 | to enable TRACE level messages:
94 | ```jshell
95 | import org.apache.logging.log4j.LogManager;
96 | import org.apache.logging.log4j.Logger;
97 | import org.apache.logging.log4j.core.config.Configurator;
98 | import org.apache.logging.log4j.Level;
99 | Logger logger = LogManager.getRootLogger();
100 | Configurator.setRootLevel(Level.TRACE);
101 |
102 | ```
103 |
104 | ## Custom alphabets
105 |
106 | Custom alphabets up to 256 characters are supported. To use an alphabet consisting of the uppercase letters A-F (radix=6), we can continue
107 | from the above code example with:
108 |
109 | ```java
110 | FF3Cipher c6 = new FF3Cipher(key, tweak, "ABCDEF");
111 | String plaintext = "BADDCAFE";
112 | String ciphertext = c6.encrypt(plaintext);
113 | String decrypted = c6.decrypt(ciphertext);
114 |
115 | System.out(String.format("{%s} -> {%s} -> {%s}", plaintext, ciphertext, decrypted);
116 | ```
117 |
118 |
119 | ## The FF3 Algorithm
120 |
121 | The FF3 algorithm is a tweakable block cipher based on an eight round Feistel cipher. A block cipher operates on fixed-length groups of bits, called blocks. A Feistel Cipher is not a specific cipher,
122 | but a design model. This FF3 Feistel encryption consisting of eight rounds of processing
123 | the plaintext. Each round applies an internal function or _round function_, followed by transformation steps.
124 |
125 | The FF3 round function uses AES encryption in ECB mode, which is performed each iteration
126 | on alternating halves of the text being encrypted. The *key* value is used only to initialize the AES cipher. Thereafter
127 | the *tweak* is used together with the intermediate encrypted text as input to the round function.
128 |
129 | FF3 uses a single-block encryption with an IV of 0, which is effectively ECB mode. AES ECB is the only block cipher function which matches the requirement of the FF3 spec.
130 |
131 | The domain size was revised in FF3-1 to radixminLen >= 1,000,000 and is represented by the constant `DOMAIN_MIN` in `FF3Cipher.java`.
132 | FF3-1 is in draft status and updated 56-bit test vectors are not yet available.
133 |
134 | ## Other FPE Algorithms
135 |
136 | Only FF1 and FF3 have been approved by NIST for format preserving encryption. There are patent claims on FF1 which allegedly include open source implementations. Given the issues raised in ["The Curse of Small Domains: New Attacks on Format-Preserving Encryption"](https://eprint.iacr.org/2018/556.pdf) by Hoang, Tessaro and Trieu in 2018, it is prudent to be very cautious about using any FPE that isn't a standard and hasn't stood up to public scrutiny.
137 |
138 | ## Build & Testing
139 |
140 | Build this project with gradle:
141 |
142 | `gradle build`
143 |
144 | Official [test vectors](https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/examples/ff3samples.pdf) for FF3 provided by NIST,
145 | are used for testing in this package. Also included are draft ACVP test vectors for FF3-1 with 56-bit tweaks.
146 |
147 | To run the unit tests, including all test vectors from the NIST specification, run the command:
148 |
149 | `gradle test`
150 |
151 | ## Performance Benchmarks
152 |
153 | Mysto FF3 was benchmarked on a MacBook Air M2 performing 105,000 tokenization per second with mixed 8 character data input.
154 |
155 | To run the performance tests:
156 |
157 | `gradle jmh`
158 |
159 | (Note: running jmh requires uncommenting jmh in the build.gradle.kts)
160 |
161 | ## Requires
162 |
163 | This project was built and tested with Java 8 and 11. It uses the javax.crypto for AES encryption in ECB mode.
164 |
165 | ## Implementation Notes
166 |
167 | This implementation follows the algorithm as outlined in the NIST specification as closely as possible, including naming.
168 |
169 | FPE can be used for sensitive data tokenization, especially with PCI and cryptographically reversible tokens. This implementation does not provide any guarantees regarding PCI DSS or other validation.
170 |
171 | The tweak is required in the initial `FF3Cipher` constructor, but can optionally be overridden in each `encrypt` and `decrypt` call. This is similar to passing an IV or nonce when creating an encryptor object.
172 |
173 | ## Author
174 |
175 | Brad Schoening
176 |
177 | ## License
178 |
179 | This project is licensed under the terms of the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0).
180 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | java
3 | // id("me.champeau.jmh") version "0.7.3"
4 | `maven-publish`
5 | signing
6 | }
7 |
8 | java.sourceCompatibility = JavaVersion.VERSION_1_8
9 |
10 | repositories {
11 | mavenCentral()
12 | }
13 |
14 | dependencies {
15 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2")
16 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2")
17 | testRuntimeOnly("org.junit.platform:junit-platform-launcher")
18 | implementation("org.apache.logging.log4j:log4j-api:2.23.1")
19 | implementation("org.apache.logging.log4j:log4j-core:2.23.1")
20 | }
21 |
22 | group = "io.github.mysto"
23 | version = "1.2.0"
24 |
25 | java {
26 | withJavadocJar()
27 | withSourcesJar()
28 | }
29 |
30 | tasks.withType {
31 | options.encoding = "UTF-8"
32 | }
33 |
34 |
35 | tasks.withType().configureEach {
36 | useJUnitPlatform()
37 | }
38 |
39 | tasks.withType {
40 | systemProperty("file.encoding", "UTF-8")
41 | testLogging {
42 | events("PASSED", "SKIPPED", "FAILED", "STANDARD_OUT", "STANDARD_ERROR")
43 | }
44 |
45 | }
46 | publishing {
47 | publications {
48 | create("mavenJava") {
49 | groupId = "io.github.mysto"
50 | artifactId = "ff3"
51 | version = "1.2.0"
52 |
53 | from(components["java"])
54 | versionMapping {
55 | usage("java-api") {
56 | fromResolutionOf("runtimeClasspath")
57 | }
58 | usage("java-runtime") {
59 | fromResolutionResult()
60 | }
61 | }
62 | pom {
63 | name.set("ff3")
64 | description.set("A Format-preserving encryption library for FF3-1")
65 | url.set("http://privacylogistics.com")
66 | licenses {
67 | license {
68 | name.set("The Apache License, Version 2.0")
69 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
70 | }
71 | }
72 | developers {
73 | developer {
74 | id.set("bschoeni")
75 | name.set("Brad Schoening")
76 | }
77 | }
78 | scm {
79 | connection.set("scm:git:git://github.com/mysto/java-fpe.git")
80 | developerConnection.set("scm:git:ssh://github.com/mysto/java-fpe.git")
81 | url.set("http://github.com/mysto/java-fpe/")
82 | }
83 | }
84 | }
85 | }
86 | repositories {
87 | maven {
88 | // change URLs to point to your repos, e.g. http://my.org/repo
89 | val releasesRepoUrl = uri(layout.buildDirectory.dir("repos"))
90 | }
91 | }
92 | }
93 |
94 | signing {
95 | sign(publishing.publications["mavenJava"])
96 | }
97 |
98 | tasks.javadoc {
99 | if (JavaVersion.current().isJava9Compatible) {
100 | (options as StandardJavadocDocletOptions).addBooleanOption("html5", true)
101 | }
102 | }
103 |
104 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "ff3"
2 |
3 |
--------------------------------------------------------------------------------
/src/jmh/java/com/privacylogistics/FF3CipherPerf.java:
--------------------------------------------------------------------------------
1 | package com.privacylogistics;
2 |
3 | /**
4 | * Format-Preserving Encryption for FF3
5 | *
6 | * Copyright (c) 2021 Schoening Consulting LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and limitations under the License.
18 | */
19 | import org.openjdk.jmh.annotations.*;
20 | import java.util.ArrayList;
21 |
22 | public class FF3CipherPerf {
23 |
24 | /*public static class CircularList extends ArrayList {
25 | @Override
26 | public E get(int index) {
27 | return super.get(index % size());
28 | }
29 | }*/
30 |
31 | /*
32 | * Benchmark Test
33 | * ToDo: use non-uniform random strings
34 | */
35 | /*@State(Scope.Thread)
36 | public static class MyState {
37 |
38 | @Setup(Level.Trial)
39 | public void doSetup() {
40 | //list = new CircularList();
41 | alist.add("12345678");
42 | alist.add("23478436");
43 | alist.add("99472512");
44 | alist.add("28830179");
45 | alist.add("23837374");
46 | }
47 | //public CircularList list;
48 | }*/
49 |
50 | @Benchmark
51 | @Fork(1)
52 | @Warmup(iterations = 3)
53 | public String testEncrypt() throws Exception {
54 | String key = "EF4359D8D580AA4F7F036D6F04FC6A94";
55 | String tweak = "D8E7920AFA330A73";
56 | String plaintext = alist.get(n++%5);
57 | FF3Cipher c = new FF3Cipher(key, tweak, 10);
58 | return c.encrypt(plaintext);
59 | }
60 |
61 | public static int n = 0;
62 | //public static CircularList list = new CircularList();
63 | public static ArrayList alist = new ArrayList();
64 | static {
65 | alist = new ArrayList();
66 | alist.add("12345678");
67 | alist.add("23478436");
68 | alist.add("99472512");
69 | alist.add("28830179");
70 | alist.add("23837374");
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/com/privacylogistics/FF3Cipher.java:
--------------------------------------------------------------------------------
1 | package com.privacylogistics;
2 |
3 | /*
4 | * Format-Preserving Encryption for FF3
5 | *
6 | * Copyright (c) 2021 Schoening Consulting LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software distributed under the
15 | * License is distributed on an "AS IS" BASIS WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
16 | * either express or implied.
17 | * See the License for the specific language governing permissions and limitations under the License.
18 | */
19 |
20 | import java.nio.charset.StandardCharsets;
21 | import javax.crypto.*;
22 | import javax.crypto.spec.SecretKeySpec;
23 | import java.math.BigInteger;
24 | import java.security.InvalidKeyException;
25 | import java.security.NoSuchAlgorithmException;
26 | import java.util.Arrays;
27 |
28 | import org.apache.logging.log4j.LogManager;
29 | import org.apache.logging.log4j.Logger;
30 |
31 | /**
32 | * Class FF3Cipher implements the FF3 format-preserving encryption algorithm
33 | */
34 | public class FF3Cipher {
35 | /**
36 | * Constructor with default radix of 10.
37 | *
38 | * @param key encryption key used to initialize AES ECB
39 | * @param tweak used in each round and split into right and left sides
40 | */
41 | public FF3Cipher(String key, String tweak) {
42 | this(key, tweak, 10);
43 | }
44 |
45 | /**
46 | * Constructor with default radix of 10.
47 | *
48 | * @param key encryption key used to initialize AES ECB
49 | * @param tweak used in each round and split into right and left sides
50 | */
51 | public FF3Cipher(byte[] key, byte[] tweak) {
52 | this(key, tweak, 10);
53 | }
54 |
55 | /**
56 | * Constructor with a custom alphabet
57 | *
58 | * @param key encryption key used to initialize AES ECB
59 | * @param tweak used in each round and split into right and left sides
60 | * @param alphabet the cipher alphabet
61 | */
62 | public FF3Cipher(byte[] key, byte[] tweak, String alphabet) {
63 | this.alphabet = alphabet;
64 | this.radix = alphabet.length();
65 |
66 | // Calculate range of supported message lengths [minLen..maxLen]
67 | // radix 10: 6 ... 56, 26: 5 ... 40, 36: 4 .. 36
68 |
69 | // Per revised spec, radix^minLength >= 1,000,000
70 | this.minLen = (int) Math.ceil(Math.log(DOMAIN_MIN) / Math.log(radix));
71 |
72 | // We simplify the specs log[radix](2^96) to 96/log2(radix) using the log base change rule
73 | this.maxLen = (int) (2 * Math.floor(Math.log(Math.pow(2, 96)) / Math.log(radix)));
74 | // ToDo: With log2 we could further simplify this
75 | // this.maxLen = (int) (2 * Math.floor(96/Math.log2(radix)));
76 |
77 | // Check if the key is 128, 192, or 256 bits = 16, 24, or 32 bytes
78 | if (key.length != 16 && key.length != 24 && key.length != 32) {
79 | throw new IllegalArgumentException("key length " + key.length + " but must be 128, 192, or 256 bits");
80 | }
81 |
82 | // While FF3 allows radices in [2, 2^16], currently only tested up to 64
83 | if ((radix < 2) || (radix > MAX_RADIX)) {
84 | throw new IllegalArgumentException("radix must be between 2 and 256, inclusive");
85 | }
86 |
87 | // Make sure 2 <= minLength <= maxLength < 2*floor(log base radix of 2^96) is satisfied
88 | if ((this.minLen < 2) || (this.maxLen < this.minLen)) {
89 | throw new IllegalArgumentException("minLen or maxLen invalid, adjust your radix");
90 | }
91 |
92 | this.defaultTweak = tweak;
93 |
94 | // AES block cipher in ECB mode with the block size derived based on the length of the key
95 | // Always use the reversed key since Encrypt and Decrypt call cipher expecting that
96 | // Feistel ciphers use the same func for encrypt/decrypt, so mode is always ENCRYPT_MODE
97 |
98 | try {
99 | byte[] reversedKey = key.clone();
100 | reverseBytes(reversedKey);
101 | SecretKeySpec keySpec = new SecretKeySpec(reversedKey, "AES");
102 | aesCipher = Cipher.getInstance("AES/ECB/NoPadding");
103 | aesCipher.init(Cipher.ENCRYPT_MODE, keySpec);
104 | } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
105 | // this could happen if the JRE doesn't have the ciphers
106 | throw new RuntimeException(e);
107 | }
108 | }
109 |
110 | /**
111 | * Constructor with a custom alphabet
112 | *
113 | * @param key encryption key used to initialize AES ECB
114 | * @param tweak used in each round and split into right and left sides
115 | * @param alphabet the cipher alphabet
116 | */
117 | public FF3Cipher(String key, String tweak, String alphabet) {
118 | this(hexStringToByteArray(key), hexStringToByteArray(tweak), alphabet);
119 | }
120 |
121 | /**
122 | * Constructor with a standardized radix
123 | *
124 | * @param key encryption key used to initialize AES ECB
125 | * @param tweak used in each round and split into right and left sides
126 | * @param radix the domain of the alphabet, 10, 26 or 36
127 | */
128 | public FF3Cipher(String key, String tweak, int radix) {
129 | this(hexStringToByteArray(key), hexStringToByteArray(tweak), alphabetForBase(radix));
130 | }
131 |
132 | /**
133 | * Constructor with a standardized radix
134 | *
135 | * @param key encryption key used to initialize AES ECB
136 | * @param tweak used in each round and split into right and left sides
137 | * @param radix the domain of the alphabet, 10, 26 or 36
138 | */
139 | public FF3Cipher(byte[] key, byte[] tweak, int radix) {
140 | this(key, tweak, alphabetForBase(radix));
141 | }
142 |
143 | public int getMinMessageLength() {
144 | return minLen;
145 | }
146 |
147 | public int getMaxMessageLength() {
148 | return maxLen;
149 | }
150 |
151 | /**
152 | * Encrypt a value using default tweak
153 | * @param plaintext a plaintext to encrypt
154 | * @return the ciphertext
155 | * @throws BadPaddingException internal error
156 | * @throws IllegalBlockSizeException internal error
157 | */
158 | public String encrypt(String plaintext) throws BadPaddingException, IllegalBlockSizeException {
159 | return encrypt(plaintext, this.defaultTweak);
160 | }
161 |
162 | /**
163 | * Encrypt a value
164 | * @param plaintext a plaintext to encrypt
165 | * @param tweak a local tweak for encrypting
166 | * @return the ciphertext
167 | * @throws BadPaddingException internal error
168 | * @throws IllegalBlockSizeException internal error
169 | */
170 | @SuppressWarnings("unused")
171 | public String encrypt(String plaintext, String tweak) throws BadPaddingException, IllegalBlockSizeException {
172 | return encrypt(plaintext, hexStringToByteArray(tweak));
173 | }
174 |
175 | /**
176 | * Encrypt a value
177 | * @param plaintext a plaintext to encrypt
178 | * @param tweak a local tweak for encrypting
179 | * @return the ciphertext
180 | * @throws BadPaddingException internal error
181 | * @throws IllegalBlockSizeException internal error
182 | */
183 | public String encrypt(String plaintext, byte[] tweak) throws BadPaddingException, IllegalBlockSizeException {
184 | int n = plaintext.length();
185 |
186 | // Check if message length is within minLength and maxLength bounds
187 | if ((n < this.minLen) || (n > this.maxLen)) {
188 | throw new IllegalArgumentException(String.format("message length %d is not within min %d and max %d bounds",
189 | n, this.minLen, this.maxLen));
190 | }
191 |
192 | // Calculate split point
193 | int u = (int) Math.ceil(n / 2.0);
194 | int v = n - u;
195 |
196 | // Split the message
197 | char[] A = new char[u];
198 | char[] B = new char[v];
199 | plaintext.getChars(0, u, A, 0);
200 | plaintext.getChars(u, plaintext.length(), B, 0);
201 |
202 | logger.trace("r {} A {} B {}", this.radix, A, B);
203 |
204 | if ((tweak.length != TWEAK_LEN) && (tweak.length != TWEAK_LEN_NEW)) {
205 | throw new IllegalArgumentException(String.format("tweak length %d is invalid: tweak must be 56 or 64 bits",
206 | tweak.length));
207 | }
208 |
209 | // Calculate the tweak
210 | logger.trace("tweak: {}", () -> byteArrayToHexString(tweak));
211 |
212 | byte[] tweak64 = (tweak.length == TWEAK_LEN_NEW) ?
213 | calculateTweak64_FF3_1(tweak) : tweak;
214 |
215 | byte[] Tl = Arrays.copyOf(tweak64, HALF_TWEAK_LEN);
216 | byte[] Tr = Arrays.copyOfRange(tweak64, HALF_TWEAK_LEN, TWEAK_LEN);
217 |
218 | // P is always 16 bytes
219 | byte[] P;
220 |
221 | // Pre-calculate the modulus since it's only one of 2 values,
222 | // depending on whether it is even or odd
223 |
224 | BigInteger modU = BigInteger.valueOf(this.radix).pow(u);
225 | BigInteger modV = BigInteger.valueOf(this.radix).pow(v);
226 | logger.trace("u {} v {} modU: {} modV: {}", u, v, modU, modV);
227 | logger.trace("tL: {} tR: {}", () -> byteArrayToHexString(Tl), () -> byteArrayToHexString(Tr));
228 |
229 | for (byte i = 0; i < NUM_ROUNDS; ++i) {
230 | int m;
231 | BigInteger c;
232 | byte[] W;
233 |
234 | // Determine alternating Feistel round side, right or left
235 | if (i % 2 == 0) {
236 | m = u;
237 | W = Tr;
238 | } else {
239 | m = v;
240 | W = Tl;
241 | }
242 |
243 | // P is fixed-length 16 bytes
244 | P = calculateP(i, this.alphabet, W, B);
245 | reverseBytes(P);
246 |
247 | // Calculate S by operating on P in place
248 | byte[] S = this.aesCipher.doFinal(P);
249 | reverseBytes(S);
250 | logger.trace("\tS: {}", () -> byteArrayToHexString(S));
251 |
252 | BigInteger y = new BigInteger(byteArrayToHexString(S), 16);
253 |
254 | // Calculate c
255 | c = decode_int_r(A, alphabet);
256 |
257 | c = c.add(y);
258 |
259 | if (i % 2 == 0) {
260 | c = c.mod(modU);
261 | } else {
262 | c = c.mod(modV);
263 | }
264 |
265 | logger.trace("\tm: {} A: {} c: {} y: {}", m, A, c, y);
266 |
267 | char[] C = encode_int_r(c, this.alphabet, m);
268 |
269 | // Final steps
270 | A = B;
271 | B = C;
272 | logger.trace("A: {} B: {}", A, B);
273 | }
274 | return new String(A) + new String(B);
275 | }
276 |
277 | /**
278 | * Decrypt a value using default tweak
279 | * @param ciphertext a ciphertext to decrypt
280 | * @return the plaintext
281 | * @throws BadPaddingException internal error
282 | * @throws IllegalBlockSizeException internal error
283 | */
284 | public String decrypt(String ciphertext) throws BadPaddingException, IllegalBlockSizeException {
285 | return decrypt(ciphertext, this.defaultTweak);
286 | }
287 |
288 | /**
289 | * Decrypt a value
290 | * @param ciphertext a ciphertext to decrypt
291 | * @param tweak a local tweak for decrypting
292 | * @return the plaintext
293 | * @throws BadPaddingException internal error
294 | * @throws IllegalBlockSizeException internal error
295 | */
296 | @SuppressWarnings("unused")
297 | public String decrypt(String ciphertext, String tweak) throws BadPaddingException, IllegalBlockSizeException {
298 | return decrypt(ciphertext, hexStringToByteArray(tweak));
299 | }
300 |
301 | /**
302 | * Decrypt a value
303 | * @param ciphertext a ciphertext to decrypt
304 | * @param tweak a local tweak for decrypting
305 | * @return the plaintext
306 | * @throws BadPaddingException internal error
307 | * @throws IllegalBlockSizeException internal error
308 | */
309 | public String decrypt(String ciphertext, byte[] tweak) throws BadPaddingException, IllegalBlockSizeException {
310 | int n = ciphertext.length();
311 |
312 | // Check if message length is within minLength and maxLength bounds
313 | if ((n < this.minLen) || (n > this.maxLen)) {
314 | throw new IllegalArgumentException(String.format("message length %d is not within min %d and max %d bounds",
315 | n, this.minLen, this.maxLen));
316 | }
317 |
318 | // Calculate split point
319 | int u = (int) Math.ceil(n / 2.0);
320 | int v = n - u;
321 |
322 | // Split the message
323 | char[] A = new char[u];
324 | char[] B = new char[v];
325 | ciphertext.getChars(0, u, A, 0);
326 | ciphertext.getChars(u, ciphertext.length(), B, 0);
327 |
328 | if ((tweak.length != TWEAK_LEN) && (tweak.length != TWEAK_LEN_NEW)) {
329 | throw new IllegalArgumentException(String.format("tweak length %d is invalid: tweak must be 56 or 64 bits",
330 | tweak.length));
331 | }
332 |
333 | // Calculate the tweak
334 | logger.trace("tweak: {}", () -> byteArrayToHexString(tweak));
335 |
336 | byte[] tweak64 = (tweak.length == TWEAK_LEN_NEW) ?
337 | calculateTweak64_FF3_1(tweak) : tweak;
338 |
339 | byte[] Tl = Arrays.copyOf(tweak64, HALF_TWEAK_LEN);
340 | byte[] Tr = Arrays.copyOfRange(tweak64, HALF_TWEAK_LEN, TWEAK_LEN);
341 |
342 | // P is always 16 bytes
343 | byte[] P;
344 |
345 | // Pre-calculate the modulus since it's only one of 2 values,
346 | // depending on whether it is even or odd
347 |
348 | BigInteger modU = BigInteger.valueOf(this.radix).pow(u);
349 | BigInteger modV = BigInteger.valueOf(this.radix).pow(v);
350 | logger.trace("modU: {} modV: {}", modU, modV);
351 | logger.trace("tL: {} tR: {}", () -> byteArrayToHexString(Tl), () -> byteArrayToHexString(Tr));
352 |
353 | for (byte i = (byte) (NUM_ROUNDS - 1); i >= 0; --i) {
354 | int m;
355 | BigInteger c;
356 | byte[] W;
357 |
358 | // Determine alternating Feistel round side, right or left
359 | if (i % 2 == 0) {
360 | m = u;
361 | W = Tr;
362 | } else {
363 | m = v;
364 | W = Tl;
365 | }
366 |
367 | // P is fixed-length 16 bytes
368 | P = calculateP(i, this.alphabet, W, A);
369 | reverseBytes(P);
370 |
371 | // Calculate S by operating on P in place
372 | byte[] S = this.aesCipher.doFinal(P);
373 | reverseBytes(S);
374 | logger.trace("\tS: {}", () -> byteArrayToHexString(S));
375 |
376 | BigInteger y = new BigInteger(byteArrayToHexString(S), 16);
377 |
378 | // Calculate c
379 | c = decode_int_r(B, alphabet);
380 |
381 | c = c.subtract(y);
382 |
383 | if (i % 2 == 0) {
384 | c = c.mod(modU);
385 | } else {
386 | c = c.mod(modV);
387 | }
388 |
389 | logger.trace("\tm: {} B: {} c: {} y: {}", m, B, c, y);
390 |
391 | char[] C = encode_int_r(c, this.alphabet, m);
392 |
393 | // Final steps
394 | B = A;
395 | A = C;
396 | logger.trace("A: {} B: {}", A, B);
397 | }
398 | return new String(A) + new String(B);
399 | }
400 |
401 | /**
402 | * For FF3-1, calculate a 64-bit tweak by transforming a 56-bit tweak
403 | * @param tweak56 an input 56-bit tweak
404 | * @return the reconstituted tweak
405 | */
406 | protected static byte[] calculateTweak64_FF3_1(byte[] tweak56)
407 | {
408 | byte[] tweak64 = new byte[8];
409 | tweak64[0] = tweak56[0];
410 | tweak64[1] = tweak56[1];
411 | tweak64[2] = tweak56[2];
412 | tweak64[3] = (byte)(tweak56[3] & 0xF0);
413 | tweak64[4] = tweak56[4];
414 | tweak64[5] = tweak56[5];
415 | tweak64[6] = tweak56[6];
416 | tweak64[7] = (byte)((tweak56[3] & 0x0F) << 4);
417 |
418 | return tweak64;
419 | }
420 |
421 | /**
422 | * Calculate P, an intermediate value
423 | * @param i an int
424 | * @param alphabet an alphabet
425 | * @param W a byte array
426 | * @param B a string value
427 | * @return a byte array
428 | */
429 | protected static byte[] calculateP(int i, String alphabet, byte[] W, char[] B) {
430 |
431 | byte[] P = new byte[BLOCK_SIZE]; // P is always 16 bytes, zero initialized
432 |
433 | // Calculate P by XORing W, i into the first 4 bytes of P
434 | // it only requires 1 byte, rest are 0 padding bytes
435 | // Anything XOR 0 is itself, so only need to XOR the last byte
436 |
437 | P[0] = W[0];
438 | P[1] = W[1];
439 | P[2] = W[2];
440 | P[3] = (byte) (W[3] ^ i);
441 |
442 | // The remaining 12 bytes of P are copied from reverse(B) with padding
443 |
444 | BigInteger val = decode_int_r(B, alphabet);
445 | byte[] bBytes = val.toByteArray();
446 |
447 | // BigInteger's toByteArray may return a 13th sign byte, but we consider the value unsigned
448 | if (bBytes.length > 12) {
449 | System.arraycopy(bBytes, 1, P, (BLOCK_SIZE - 12), 12);
450 | } else {
451 | System.arraycopy(bBytes, 0, P, (BLOCK_SIZE - bBytes.length), bBytes.length);
452 | }
453 | logger.trace("round: {} P: {} W: {}", () -> i, () -> byteArrayToHexString(P), () -> byteArrayToHexString(W));
454 | return P;
455 | }
456 |
457 | /**
458 | * Reverse an immutable string
459 | * @param s the original string
460 | * @return the new string
461 | */
462 | protected static String reverseString(String s) {
463 | return new StringBuilder(s).reverse().toString();
464 | }
465 |
466 | /**
467 | * Reverse a byte array in-place
468 | * @param b a mutable byte array
469 | */
470 | protected static void reverseBytes(byte[] b) {
471 | for (int i = 0; i < b.length / 2; i++) {
472 | byte temp = b[i];
473 | b[i] = b[b.length - i - 1];
474 | b[b.length - i - 1] = temp;
475 | }
476 | }
477 |
478 | /**
479 | * Returns a byte array containing hexadecimal values parsed from the string
480 | * @param s a character string containing hexadecimal digits
481 | * @return a byte array with the values parsed from the string
482 | */
483 | public static byte[] hexStringToByteArray(String s) {
484 | byte[] data = new byte[s.length() / 2];
485 | for (int i = 0; i < s.length(); i += 2) {
486 | data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
487 | + Character.digit(s.charAt(i+1), 16));
488 | }
489 | return data;
490 | }
491 |
492 | /**
493 | * Java 17 has java.util.HexFormat
494 | * @param byteArray a byte array
495 | * @return a hex string encoding of a number
496 | */
497 | protected static String byteArrayToHexString(byte[] byteArray) {
498 | byte[] hexChars = new byte[byteArray.length * 2];
499 | for (int j = 0; j < byteArray.length; j++) {
500 | int v = byteArray[j] & 0xFF;
501 | hexChars[j * 2] = (byte) HEX_ARRAY[v >>> 4];
502 | hexChars[j * 2 + 1] = (byte) HEX_ARRAY[v & 0x0F];
503 | }
504 | return new String(hexChars, StandardCharsets.UTF_8);
505 | }
506 |
507 |
508 | /**
509 | * Return a char[] representation of a number in the given base system
510 | * - the char[] is right padded with zeros to length
511 | * - the char[] is returned in reversed order expected by the calling cryptographic function
512 | * i.e., the decimal value 123 in five decimal places would be '32100'
513 | *
514 | * examples:
515 | * encode_int_r(10, 16,2)
516 | * 'A0'
517 | * @param n the integer number
518 | * @param alphabet the alphabet used for encoding
519 | * @param length the length used for padding the output string
520 | * @return a char[] encoding of the number
521 | */
522 | protected static char[] encode_int_r(BigInteger n, String alphabet, int length) {
523 |
524 | char[] x = new char[length];
525 | int i=0;
526 |
527 | BigInteger bbase = BigInteger.valueOf(alphabet.length());
528 | while (n.compareTo(bbase) >= 0) {
529 | BigInteger b = n.mod(bbase);
530 | n = n.divide(bbase);
531 | x[i++] = alphabet.charAt(b.intValue());
532 | }
533 | x[i++] = alphabet.charAt(n.intValue());
534 |
535 | // pad with zeros-index value if necessary
536 | while (i < length) {
537 | x[i++] = alphabet.charAt(0);
538 | }
539 | return x;
540 | }
541 |
542 | /**
543 | * Decode a base X char[] representation into an integer.
544 | * - the BigInteger is returned in reversed order expected by the calling cryptographic function
545 | * @param str the original char[]
546 | * @param alphabet the alphabet and radix (len(alphabet)) for decoding
547 | * @return an integer value of the encoded char[]
548 | */
549 | protected static BigInteger decode_int_r(char[] str, String alphabet) {
550 | BigInteger base = BigInteger.valueOf(alphabet.length());
551 | BigInteger num = BigInteger.ZERO;
552 | for (int i = 0; i < str.length; i++) {
553 | char ch = str[i];
554 | num = num.add(base.pow(i).multiply(BigInteger.valueOf(alphabet.indexOf(ch))));
555 | }
556 | return num;
557 | }
558 |
559 | /**
560 | * Return the canonical alphabet for a given base
561 | * @param base a base
562 | * @return the canonical alphabet for the base
563 | */
564 | protected static String alphabetForBase(int base) {
565 | switch (base) {
566 | case 10:
567 | return DIGITS;
568 | case 36:
569 | return DIGITS + ASCII_UPPERCASE;
570 | case 62:
571 | return DIGITS + ASCII_UPPERCASE + ASCII_LOWERCASE;
572 | default:
573 | throw new RuntimeException("Unsupported radix");
574 | }
575 | }
576 |
577 | private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
578 | private static final int NUM_ROUNDS = 8;
579 | private static final int BLOCK_SIZE = 16; // aes.BlockSize
580 | private static final int TWEAK_LEN = 8; // Original FF3 64-bit tweak length
581 | private static final int TWEAK_LEN_NEW = 7; // FF3-1 56-bit tweak length
582 | private static final int HALF_TWEAK_LEN = TWEAK_LEN/2;
583 | private static final int MAX_RADIX = 256;
584 | private static final Logger logger = LogManager.getLogger(FF3Cipher.class.getName());
585 |
586 | /** The recommendation in Draft SP 800-38G was strengthened to a requirement in Draft SP 800-38G Revision 1:
587 | the minimum domain size for FF1 and FF3-1 is one million */
588 | public static final int DOMAIN_MIN = 1000000; // 1M
589 | public static final String DIGITS = ("0123456789");
590 | public static final String ASCII_LOWERCASE = ("abcdefghijklmnopqrstuvwxyz");
591 | public static final String ASCII_UPPERCASE = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
592 |
593 | private final int radix;
594 | private final String alphabet;
595 | private byte[] defaultTweak;
596 | private final int minLen;
597 | private final int maxLen;
598 | private final Cipher aesCipher;
599 | }
600 |
--------------------------------------------------------------------------------
/src/test/java/com/privacylogistics/FF3CipherTest.java:
--------------------------------------------------------------------------------
1 | package com.privacylogistics;
2 |
3 | /*
4 | * Format-Preserving Encryption for FF3
5 | *
6 | * Copyright (c) 2021 Schoening Consulting LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and limitations under the License.
18 | */
19 |
20 | import org.junit.jupiter.api.Test;
21 | import org.junit.jupiter.api.condition.DisabledOnJre;
22 |
23 | import static com.privacylogistics.FF3Cipher.*;
24 | import static org.junit.jupiter.api.Assertions.*;
25 | import org.junit.jupiter.api.condition.JRE;
26 |
27 | import java.math.BigInteger;
28 |
29 | public class FF3CipherTest {
30 |
31 | /*
32 | * NIST Test Vectors for 128, 198, and 256 bit modes
33 | * https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/examples/ff3samples.pdf
34 | */
35 |
36 | static int Tradix=0, Tkey=1, Ttweak=2, Tplaintext=3, Tciphertext=4;
37 |
38 | static String[][] TestVectors = {
39 | // AES-128 - radix, key, tweak, plaintext, ciphertext
40 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A94", "D8E7920AFA330A73",
41 | "890121234567890000", "750918814058654607"
42 | },
43 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A94", "9A768A92F60E12D8",
44 | "890121234567890000", "018989839189395384"
45 | },
46 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A94", "D8E7920AFA330A73",
47 | "89012123456789000000789000000", "48598367162252569629397416226"
48 | },
49 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A94", "0000000000000000",
50 | "89012123456789000000789000000", "34695224821734535122613701434"
51 | },
52 | { "26", "EF4359D8D580AA4F7F036D6F04FC6A94", "9A768A92F60E12D8",
53 | "0123456789abcdefghi", "g2pk40i992fn20cjakb"
54 | },
55 |
56 | // AES-192 - radix, key, tweak, plaintext, ciphertext
57 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "D8E7920AFA330A73",
58 | "890121234567890000", "646965393875028755"
59 | },
60 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "9A768A92F60E12D8",
61 | "890121234567890000", "961610514491424446"
62 | },
63 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "D8E7920AFA330A73",
64 | "89012123456789000000789000000", "53048884065350204541786380807"
65 | },
66 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "0000000000000000",
67 | "89012123456789000000789000000", "98083802678820389295041483512"
68 | },
69 | { "26", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "9A768A92F60E12D8",
70 | "0123456789abcdefghi", "i0ihe2jfj7a9opf9p88"
71 | },
72 |
73 | // AES-256 - radix, key, tweak, plaintext, ciphertext
74 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "D8E7920AFA330A73",
75 | "890121234567890000", "922011205562777495"
76 | },
77 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "9A768A92F60E12D8",
78 | "890121234567890000", "504149865578056140"
79 | },
80 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "D8E7920AFA330A73",
81 | "89012123456789000000789000000", "04344343235792599165734622699"
82 | },
83 | { "10", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "0000000000000000",
84 | "89012123456789000000789000000", "30859239999374053872365555822"
85 | },
86 | { "26", "EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "9A768A92F60E12D8",
87 | "0123456789abcdefghi", "p0b2godfja9bhb7bk38"
88 | }
89 | };
90 |
91 | static int Uradix=0, Ualphabet=1, Ukey=2, Utweak=3, Uplaintext=4, Uciphertext=5;
92 |
93 | static String[][] TestVectors_ACVP_AES_FF3_1 = {
94 | // AES-128 tg: 1-3 tc: 1-2 radix, alphabet, key, tweak, plaintext, ciphertext
95 | {"10", "0123456789", "2DE79D232DF5585D68CE47882AE256D6", "CBD09280979564",
96 | "3992520240", "8901801106"
97 | },
98 | {"10", "0123456789", "01C63017111438F7FC8E24EB16C71AB5", "C4E822DCD09F27",
99 | "60761757463116869318437658042297305934914824457484538562",
100 | "35637144092473838892796702739628394376915177448290847293"
101 | },
102 | {"26", "abcdefghijklmnopqrstuvwxyz", "718385E6542534604419E83CE387A437", "B6F35084FA90E1",
103 | "wfmwlrorcd", "ywowehycyd"
104 | },
105 | {"26", "abcdefghijklmnopqrstuvwxyz", "DB602DFF22ED7E84C8D8C865A941A238", "EBEFD63BCC2083",
106 | "kkuomenbzqvggfbteqdyanwpmhzdmoicekiihkrm",
107 | "belcfahcwwytwrckieymthabgjjfkxtxauipmjja"
108 | },
109 | {"64", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/", "AEE87D0D485B3AFD12BD1E0B9D03D50D",
110 | "5F9140601D224B",
111 | "ixvuuIHr0e", "GR90R1q838"
112 | },
113 | {"64", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/", "7B6C88324732F7F4AD435DA9AD77F917",
114 | "3F42102C0BAB39",
115 | "21q1kbbIVSrAFtdFWzdMeIDpRqpo", "cvQ/4aGUV4wRnyO3CHmgEKW5hk8H"
116 | }
117 | };
118 |
119 | @Test
120 | public void testConstructors() {
121 | String keyStr = "EF4359D8D580AA4F7F036D6F04FC6A94";
122 | String tweakStr = "D8E7920AFA330A73";
123 | byte[] keyBytes = hexStringToByteArray(keyStr);
124 | byte[] tweakBytes = hexStringToByteArray(tweakStr);
125 |
126 | FF3Cipher cs0 = new FF3Cipher(keyStr, tweakStr);
127 | FF3Cipher cs1 = new FF3Cipher(keyStr, tweakStr, 62);
128 | FF3Cipher cs2 = new FF3Cipher(keyStr, tweakStr, "0123456789");
129 | FF3Cipher cb0 = new FF3Cipher(keyBytes, tweakBytes);
130 | FF3Cipher cb1 = new FF3Cipher(keyBytes, tweakBytes, 62);
131 | FF3Cipher cb2 = new FF3Cipher(keyBytes, tweakBytes, "0123456789");
132 |
133 | assertNotNull(cs0);
134 | assertNotNull(cs1);
135 | assertNotNull(cs2);
136 | assertNotNull(cb0);
137 | assertNotNull(cb1);
138 | assertNotNull(cb2);
139 | }
140 |
141 | @Test
142 | public void testByteArrayUtils() {
143 | String hexstr = "BADA55";
144 | byte[] bytestr = {(byte) 0xba, (byte) 0xda, (byte) 0x55};
145 | byte[] hex = FF3Cipher.hexStringToByteArray(hexstr);
146 | assertArrayEquals(hex, bytestr);
147 | String str = FF3Cipher.byteArrayToHexString(hex);
148 | assertEquals(hexstr, str);
149 | }
150 |
151 | @Test
152 | public void testCalculateP() {
153 | // NIST Sample #1, round 0
154 | int i = 0;
155 | String alphabet = "0123456789";
156 | String B = "567890000";
157 | byte[] W = FF3Cipher.hexStringToByteArray("FA330A73");
158 | byte[] P = FF3Cipher.calculateP(i, alphabet, W, B.toCharArray());
159 | assertArrayEquals(P, new byte[]
160 | {(byte) 250, 51, 10, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, (byte) 129, (byte) 205});
161 | }
162 |
163 | /*
164 | ToDo: replace this with a value-not-in radix error
165 | @Test(expected = NumberFormatException.class)
166 | public void testInvalidPlaintext() throws Exception {
167 | FF3Cipher c = new FF3Cipher("EF4359D8D580AA4F7F036D6F04FC6A94", "D8E7920AFA330A73", 10);
168 | c.encrypt("222-22-2222");
169 | }*/
170 |
171 | @Test
172 | public void testEncodeBigInt() {
173 | assertEquals("101", reverseString(new String(encode_int_r(BigInteger.valueOf(5), "01", 3))));
174 | assertEquals("11", reverseString(new String(encode_int_r(BigInteger.valueOf(6), "01234", 2))));
175 | assertEquals("00012", reverseString(new String(encode_int_r(BigInteger.valueOf(7), "01234", 5))));
176 | assertEquals("a", reverseString(new String(encode_int_r(BigInteger.valueOf(10), "0123456789abcdef", 1))));
177 | assertEquals("20", reverseString(new String(encode_int_r(BigInteger.valueOf(32), "0123456789abcdef", 2))));
178 | }
179 |
180 | @Test
181 | public void testDecodeInt() {
182 | assertEquals(BigInteger.valueOf(321), (decode_int_r("123".toCharArray(), "0123456789")));
183 | assertEquals(BigInteger.valueOf(101), (decode_int_r("101".toCharArray(), "0123456789")));
184 | assertEquals(BigInteger.valueOf(101), (decode_int_r("10100".toCharArray(), "0123456789")));
185 | assertEquals(BigInteger.valueOf(0x02), (decode_int_r("20".toCharArray(), "0123456789abcdef")));
186 | assertEquals(BigInteger.valueOf(0xAA), (decode_int_r("aa".toCharArray(), "0123456789abcdef")));
187 | assertEquals(new BigInteger("2297305934914824457484538562"), (decode_int_r("2658354847544284194395037922".toCharArray(), "0123456789")));
188 | }
189 |
190 | @Test
191 | public void testReverseBytes() {
192 | byte [] mutableArray = new byte[] {1};
193 | reverseBytes(mutableArray);
194 | assertArrayEquals(new byte[]{1}, mutableArray);
195 | mutableArray = new byte[] {1,2,3};
196 | reverseBytes(mutableArray);
197 | assertArrayEquals(new byte[]{3,2,1}, mutableArray);
198 | mutableArray = new byte[] {1,2,3,4};
199 | reverseBytes(mutableArray);
200 | assertArrayEquals(new byte[]{4,3,2,1}, mutableArray);
201 | }
202 | @Test
203 | public void testNistFF3() throws Exception {
204 | // NIST FF3-AES 128, 192, 256
205 | for( String[] testVector : TestVectors) {
206 | // NIST radix to alphabet mappings are non-standard, so we special case them here
207 | FF3Cipher c;
208 | int r = Integer.parseInt(testVector[Tradix]);
209 | switch (r) {
210 | case 2:
211 | c = new FF3Cipher(testVector[Tkey], testVector[Ttweak], 2);
212 | break;
213 | case 10:
214 | c = new FF3Cipher(testVector[Tkey], testVector[Ttweak], 10);
215 | break;
216 | case 26:
217 | c = new FF3Cipher(testVector[Tkey], testVector[Ttweak], FF3Cipher.DIGITS + "abcdefghijklmnop");
218 | break;
219 | default:
220 | throw new RuntimeException(String.format("Unsupported radix %d", r));
221 | }
222 | String pt = testVector[Tplaintext], ct = testVector[Tciphertext];
223 | String ciphertext = c.encrypt(pt);
224 | String plaintext = c.decrypt(ciphertext);
225 | assertEquals(ct, ciphertext);
226 | assertEquals(pt, plaintext);
227 | }
228 | }
229 |
230 | @Test
231 | public void testAcvpFF3_1() throws Exception {
232 | // ACVP FF3-AES 128, 192, 256
233 | for( String[] testVector : TestVectors_ACVP_AES_FF3_1) {
234 | int radix = Integer.parseInt(testVector[Uradix]);
235 | FF3Cipher c;
236 | if (radix == 10) {
237 | c = new FF3Cipher(testVector[Ukey], testVector[Utweak], radix);
238 | } else {
239 | c = new FF3Cipher(testVector[Ukey], testVector[Utweak], testVector[Ualphabet]);
240 | }
241 | String pt = testVector[Uplaintext], ct = testVector[Uciphertext];
242 | String ciphertext = c.encrypt(pt);
243 | String plaintext = c.decrypt(ciphertext);
244 | assertEquals(ct, ciphertext);
245 | assertEquals(pt, plaintext);
246 | }
247 | }
248 |
249 | @Test
250 | public void testFF3_1_str() throws Exception {
251 | // Test with 56 bit tweak
252 | String[] testVector = TestVectors[0];
253 | FF3Cipher c = new FF3Cipher(testVector[Tkey], "D8E7920AFA330A", Integer.parseInt(testVector[Tradix]));
254 | String pt = testVector[Tplaintext], ct = "477064185124354662";
255 | String ciphertext = c.encrypt(pt);
256 | String plaintext = c.decrypt(ciphertext);
257 | assertEquals(ct, ciphertext);
258 | assertEquals(pt, plaintext);
259 | }
260 |
261 | @Test
262 | public void testFF3_1_bytes() throws Exception {
263 | // Test with 56 bit tweak
264 | String[] testVector = TestVectors[0];
265 | FF3Cipher c = new FF3Cipher(hexStringToByteArray(testVector[Tkey]), hexStringToByteArray("D8E7920AFA330A"), Integer.parseInt(testVector[Tradix]));
266 | String pt = testVector[Tplaintext], ct = "477064185124354662";
267 | String ciphertext = c.encrypt(pt);
268 | String plaintext = c.decrypt(ciphertext);
269 | assertEquals(ct, ciphertext);
270 | assertEquals(pt, plaintext);
271 | }
272 |
273 | @Test
274 | @DisabledOnJre(JRE.JAVA_8)
275 | public void testCustomAlphabet() throws Exception {
276 | // Check the first NIST 128-bit test vector using superscript characters
277 | String alphabet = "⁰¹²³⁴⁵⁶⁷⁸⁹";
278 | String key = "EF4359D8D580AA4F7F036D6F04FC6A94";
279 | String tweak = "D8E7920AFA330A73";
280 | String pt = "⁸⁹⁰¹²¹²³⁴⁵⁶⁷⁸⁹⁰⁰⁰⁰";
281 | String ct = "⁷⁵⁰⁹¹⁸⁸¹⁴⁰⁵⁸⁶⁵⁴⁶⁰⁷";
282 | FF3Cipher c = new FF3Cipher(key, tweak, alphabet);
283 | String ciphertext = c.encrypt(pt);
284 | assertEquals(ct, ciphertext) ;
285 | String plaintext = c.decrypt(ciphertext);
286 | assertEquals(pt, plaintext);
287 | }
288 |
289 | @Test
290 | public void testGermanAlphabet() throws Exception {
291 | // Test the German alphabet with a radix of 70. German consists of the latin alphabet
292 | // plus four additional letters, each of which have uppercase and lowercase letters
293 |
294 | String german_alphabet = FF3Cipher.DIGITS + FF3Cipher.ASCII_LOWERCASE + FF3Cipher.ASCII_UPPERCASE + "ÄäÖöÜüẞß";
295 | String key = "EF4359D8D580AA4F7F036D6F04FC6A94";
296 | String tweak = "D8E7920AFA330A73";
297 | String pt = "liebeGrüße";
298 | String ct = "5kÖQbairXo";
299 | FF3Cipher c = new FF3Cipher(key, tweak, german_alphabet);
300 | String ciphertext = c.encrypt(pt);
301 | assertEquals(ct, ciphertext);
302 | String plaintext = c.decrypt(ciphertext);
303 | assertEquals(pt, plaintext);
304 | }
305 |
306 | @Test
307 | public void testDecodeBigIntegerWithLeadingSignByte() throws Exception {
308 | // toByteArray can generate a 13 byte array with a leading 0x00 to represent the sign
309 | // we expect max 12 bytes and need to strip that.
310 |
311 | String alphabet = FF3Cipher.ASCII_UPPERCASE + FF3Cipher.ASCII_LOWERCASE + DIGITS;
312 | String key = "2DE79D232DF5585D68CE47882AE256D6";
313 | String tweak = "CBD09280979564";
314 | String pt = "Ceciestuntestdechiffrement123cet";
315 | String ct = "0uaTPI9g49f9MMw54OvY8x5rmNcrhydM";
316 | FF3Cipher c = new FF3Cipher(key, tweak, alphabet);
317 | String ciphertext = c.encrypt(pt);
318 | assertEquals(ct, ciphertext);
319 | String plaintext = c.decrypt(ciphertext);
320 | assertEquals(pt, plaintext);
321 | }
322 |
323 | @Test
324 | void testTweakHasNoSideEffects() throws Exception {
325 | String key = "EF4359D8D580AA4F7F036D6F04FC6A94";
326 | String tweak = "D8E7920AFA330A73";
327 | String tweak2 = "0000000000000000";
328 | String alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
329 |
330 | FF3Cipher c = new FF3Cipher(key, tweak, alphabet);
331 | String pt = "Foobar";
332 |
333 | String ciphertext1 = c.encrypt(pt);
334 | String ciphertext2 = c.encrypt(pt, tweak);
335 | assertEquals(ciphertext1, ciphertext2);
336 |
337 | String ciphertext3 = c.encrypt(pt, tweak2); // this will NOT overwrite the initial tweak
338 | assertNotEquals(ciphertext1, ciphertext3); // different ciphertexts because different tweaks were used
339 |
340 | String ciphertext4 = c.encrypt(pt); // will still use the initial tweak
341 | assertEquals(ciphertext1, ciphertext4);
342 | }
343 | }
344 |
--------------------------------------------------------------------------------