diff --git a/Makefile b/Makefile index 74ea69d..e985dd0 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,10 @@ default: | help build: ## Build the project and install to you local maven repo mvn clean install +.PHONY: run-benchmark +run-benchmark: ## Run benchmark tests + mvn clean package exec:exec -Pbenchmark + .PHONY: release-dryrun release-dryrun: ## Simulate a release in order to detect any issues mvn release:prepare release:perform -Darguments="-Dmaven.deploy.skip=true" -DdryRun=true diff --git a/README.md b/README.md index 5fc0ed8..a02aba1 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,33 @@ dependencies { // TODO + +## Benchmark tests + +You can execute benchmark tests on your local machine by running `make run-benchmark`. + +The following is the results of a benchmark test run on a MacBook Pro with M1 Max CPU and 64 GB memory. + +``` +Benchmark (content) Mode Cnt Score Error Units +----------------------------------------------------------------------------------------- +EncryptBenchmark.encryptBytes 6 chars thrpt 5 142288.875 ± 2194.094 ops/s +EncryptBenchmark.encryptBytes 2 chars thrpt 5 2835886.070 ± 42774.201 ops/s +EncryptBenchmark.encryptBytes sentence thrpt 5 33434.985 ± 371.490 ops/s +EncryptBenchmark.encryptBytes long-complex thrpt 5 1352.778 ± 33.741 ops/s + +EncryptBenchmark.decryptBytes 6 chars thrpt 5 134362.809 ± 1088.432 ops/s +EncryptBenchmark.decryptBytes 2 chars thrpt 5 3664099.986 ± 10341.415 ops/s +EncryptBenchmark.decryptBytes sentence thrpt 5 31769.839 ± 113.963 ops/s +EncryptBenchmark.decryptBytes long-complex thrpt 5 1254.423 ± 31.235 ops/s +``` + +The main use case would be single word inputs, represented by the `6 chars` inputs. The `long-complex` string is +a >200 words text, with complex typography. + +The error column denotes the _confidence interval_. A low error value indicates that results +are more precise and reliable, while a high error value suggests greater variability in the measurements. + ## Known issues // TODO: Describe issue about chunking that results in up to last 3 characters not being encrypted. diff --git a/pom.xml b/pom.xml index 7ace12c..a2a38a6 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ 1.0.0 31.1-jre 2.14.1 + 1.35 1.5.1 5.9.1 1.4.5 @@ -37,7 +38,7 @@ 3.1.2 3.8.1 3.3.0 - 3.2.4 + 3.4.1 2.22.2 3.2.1 0.8.7 @@ -179,48 +180,6 @@ - - org.apache.maven.plugins - maven-shade-plugin - ${maven-shade-plugin.version} - - - package - - shade - - - - - - - - com.idealista - no.ssb.dapla.dlp.shaded.com.idealista - - - com.google.common - no.ssb.dapla.dlp.shaded.com.google.common - - - com.fasterxml.jackson - no.ssb.dapla.dlp.shaded.jackson - - - shaded - true - - - com.idealista:* - com.google.guava:* - com.fasterxml.jackson.core:* - - - false - - - - com.github.os72 protoc-jar-maven-plugin @@ -345,6 +304,85 @@ + + benchmark + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + add-source + generate-sources + + add-source + + + + src/benchmark/java + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + ${project.build.finalName}-with-dependencies + + + no.ssb.crypto.tink.fpe.benchmark.BenchmarkRunner + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + + exec + + + + + java + + -jar + ${project.build.directory}/${project.build.finalName}-with-dependencies.jar + + + + + + diff --git a/src/benchmark/java/no/ssb/crypto/tink/fpe/benchmark/BenchmarkRunner.java b/src/benchmark/java/no/ssb/crypto/tink/fpe/benchmark/BenchmarkRunner.java new file mode 100644 index 0000000..81a3f45 --- /dev/null +++ b/src/benchmark/java/no/ssb/crypto/tink/fpe/benchmark/BenchmarkRunner.java @@ -0,0 +1,16 @@ +package no.ssb.crypto.tink.fpe.benchmark; + +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +public class BenchmarkRunner +{ + public static void main(String[] args) throws Exception { + Options opt = new OptionsBuilder() + .include(EncryptBenchmark.class.getSimpleName()) + .forks(1) + .build(); + new Runner(opt).run(); + } +} diff --git a/src/benchmark/java/no/ssb/crypto/tink/fpe/benchmark/EncryptBenchmark.java b/src/benchmark/java/no/ssb/crypto/tink/fpe/benchmark/EncryptBenchmark.java new file mode 100644 index 0000000..926d41a --- /dev/null +++ b/src/benchmark/java/no/ssb/crypto/tink/fpe/benchmark/EncryptBenchmark.java @@ -0,0 +1,114 @@ +package no.ssb.crypto.tink.fpe.benchmark; + +import com.google.crypto.tink.KeysetHandle; +import no.ssb.crypto.tink.fpe.Fpe; +import no.ssb.crypto.tink.fpe.FpeConfig; +import no.ssb.crypto.tink.fpe.FpeParams; +import no.ssb.crypto.tink.fpe.UnknownCharacterStrategy; +import no.ssb.crypto.tink.fpe.util.TinkUtil; +import org.openjdk.jmh.annotations.*; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Map; + +@State(Scope.Benchmark) +public class EncryptBenchmark { + + static { + try { + FpeConfig.register(); + } + catch (GeneralSecurityException e) { + throw new RuntimeException("Error initializing Tink FPE", e); + } + } + + private final static String TINY = "2 chars"; + private final static String SHORT = "6 chars"; + private final static String MEDIUM = "sentence"; + private final static String LONG = "long-complex"; + private final static Map ENCRYPT_PARAMS = Map.of( + TINY, "AB", + SHORT, "Foobar", + MEDIUM, "If I cøuld gather Åll the stars ænd håld them in my hænd...", + LONG, "CHAPTER 1. Loomings.\n" + + "\n" + + "Call me Ishmael. Some years ago—never mind how long precisely—having\n" + + "little or no money in my purse, and nothing particular to interest me\n" + + "on shore, I thought I would sail about a little and see the watery part\n" + + "of the world. It is a way I have of driving off the spleen and\n" + + "regulating the circulation. Whenever I find myself growing grim about\n" + + "the mouth; whenever it is a damp, drizzly November in my soul; whenever\n" + + "I find myself involuntarily pausing before coffin warehouses, and\n" + + "bringing up the rear of every funeral I meet; and especially whenever\n" + + "my hypos get such an upper hand of me, that it requires a strong moral\n" + + "principle to prevent me from deliberately stepping into the street, and\n" + + "methodically knocking people’s hats off—then, I account it high time to\n" + + "get to sea as soon as I can. This is my substitute for pistol and ball.\n" + + "With a philosophical flourish Cato throws himself upon his sword; I\n" + + "quietly take to the ship. There is nothing surprising in this. If they\n" + + "but knew it, almost all men in their degree, some time or other,\n" + + "cherish very nearly the same feelings towards the ocean with me." + ); + + private final static Map DECRYPT_PARAMS = Map.of( + TINY, "AB", + SHORT, "6jZemW", + MEDIUM, "WT U 1øwyY rIbuCc Å82 o2G YHOt6 ævz råd7 7juD dk Ok mædK...", + LONG, "9jlDcz8 C. Q3tISqHV.\n" + + "\n" + + "G57B zx Di9LUBD. UFI2 iSXch doY—eBfWL qJtx ZDm 7dTJ PHPSfmy45—sF6Pjf\n" + + "lZHmGh 9b hG wHiNy Js 8G 0wNEQ, Ics ZZYLNog MqIuB9pGdt D8 LvV9NpkN fh\n" + + "Nv oR7uu, h 4NZTvwY 7 sAI0N UbVU 6gR3L P XnRx9l pIS 4ZN E3G LfDCQQ 4hPP\n" + + "rf jBT ezVVu. pH tq s tz1 H YqNS Bc WghESk1 tIJ 0BA ol9ask JQr\n" + + "BLLXeigOX4 1Yg 9cY6aotAvEu. cYZzKjPp E BilL iuH3lM CXnnIYe NPlr wgPSm\n" + + "ukS Oxyqx; MwCvypUd dB zJ j QJKY, CD5THSj WQ3kpZzl m0 dj bDsd; kjmKVR3K\n" + + "q FVR7 dQ1SFC 3Z9pts7hJRjME aswYrOq IoGTIf rFDjgR J5kUVWJOIR, l1j\n" + + "hbSpUgb4 ks uKh EzjV tp EaxQA zvWMyfp o PljS; NnJ swOE76mbmR 5q7tdWxY\n" + + "aC HgRyU xVC yIkw gH gqnIz YBA8 ii N6, JMYU vK ZTIiM6s1 f QTDUUL 2jWfV\n" + + "5ymzT5fJt Jp muIQMs0 GI lvpX Tc3mtVywBjMP pQPYr7S9 0ijn o5Y dnB6E1, Uo2\n" + + "la6PRofKXkqu 7LWnZm7m mdHEUC’p wIYG ghe—cLAo, z XuuuOKn Bz Qj69 JAM7 p8\n" + + "xoW wk 3pv YQ l55l 7s B sLd. husr lg Gu v1JilWizle IId QxQgJj 1n7 SPSt.\n" + + "A7Rd G NP0zvrUOKucMc dI3gP4jZ kbdz yMuS8X PzMWLVY X3Zi uF5 blNEE; U\n" + + "okyUfCY PG59 Tc via d1zz. 7l8h7 AF FP4aLzN 3csHeXgTC4 WX q73I. J0 qKGm\n" + + "hlu FWUr Ba, 7pe8Je CbN xW3 XW IavnU bnvYnx, BFrL kSzD Mn 3vN41,\n" + + "85tFSiB iHqq cqTERD pUE kSK9 87d3GvbO LN14wfe d1O n57mG FE4Z iL." + ); + + private final static String KEYSET_JSON_FF31_256_ALPHANUMERIC = "{\"primaryKeyId\":1720617146,\"key\":[{\"keyData\":{\"typeUrl\":\"type.googleapis.com/ssb.crypto.tink.FpeFfxKey\",\"value\":\"EiBoBeUFkoew7YJObcgcz1uOmzdhJFkPP7driAxAuS0UiRpCEAIaPkFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5\",\"keyMaterialType\":\"SYMMETRIC\"},\"status\":\"ENABLED\",\"keyId\":1720617146,\"outputPrefixType\":\"RAW\"}]}"; + + // Define sample plaintext inputs for both methods + @Param(value = {SHORT, TINY, MEDIUM, LONG}) + public String paramName; + + public byte[] plaintextBytes; + + public byte[] ciphertextBytes; + + private Fpe fpe; + private FpeParams fpeParams; + + // Prepare the byte array from plaintextString + @Setup(Level.Trial) + public void prepare() throws Exception { + KeysetHandle keysetHandle = TinkUtil.readKeyset(KEYSET_JSON_FF31_256_ALPHANUMERIC); + fpe = keysetHandle.getPrimitive(Fpe.class); + fpeParams = FpeParams.with().unknownCharacterStrategy(UnknownCharacterStrategy.SKIP); + + plaintextBytes = ENCRYPT_PARAMS.get(paramName).getBytes(StandardCharsets.UTF_8); + ciphertextBytes = DECRYPT_PARAMS.get(paramName).getBytes(StandardCharsets.UTF_8); + } + + // Benchmark for encrypt(byte[] plaintext) method + @Benchmark + public byte[] encryptBytes() throws Exception { + return fpe.encrypt(plaintextBytes, fpeParams); + } + + @Benchmark + public byte[] decryptBytes() throws Exception { + return fpe.decrypt(ciphertextBytes, fpeParams); + } + +}