├── .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 | [![Build Status](https://github.com/mysto/java-fpe/actions/workflows/build-gradle.yml/badge.svg)](https://github.com/mysto/java-fpe/actions) 2 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.mysto/ff3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.mysto/ff3) 4 | [![javadoc](https://javadoc.io/badge2/io.github.mysto/ff3/javadoc.svg)](https://javadoc.io/doc/io.github.mysto/ff3) 5 | 6 |

7 | 8 | Mysto 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 | --------------------------------------------------------------------------------