├── test ├── .travis ├── gpg.asc.enc └── mvn-settings.xml ├── .gitignore ├── NOTICE ├── HOW-TO-DEPLOY.md ├── .mvn ├── jgitver.config.xml └── extensions.xml ├── samples ├── generatedkey-sample │ ├── README.md │ ├── docker-compose.yml │ ├── runSamples.sh │ ├── generateMasterKey.sh │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── logback.xml │ │ │ └── java │ │ │ └── io │ │ │ └── quicksign │ │ │ └── kafka │ │ │ └── crypto │ │ │ └── samples │ │ │ └── generatedkey │ │ │ ├── KeyStoreBasedMasterKey.java │ │ │ ├── SampleRawConsumer.java │ │ │ ├── SamplesMain.java │ │ │ ├── AesGcmNoPaddingCryptoAlgorithm.java │ │ │ ├── SampleDecryptingConsumer.java │ │ │ └── SampleProducer.java │ ├── index.adoc │ └── pom.xml ├── keyrepo-sample │ ├── README.md │ ├── runSamples.sh │ ├── docker-compose.yml │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── logback.xml │ │ │ └── java │ │ │ └── io │ │ │ └── quicksign │ │ │ └── kafka │ │ │ └── crypto │ │ │ └── samples │ │ │ └── keyrepo │ │ │ ├── SampleKeyNameExtractor.java │ │ │ ├── SampleKeyRepository.java │ │ │ ├── SampleKeyNameObfuscator.java │ │ │ ├── SampleRawConsumer.java │ │ │ ├── SamplesMain.java │ │ │ ├── SampleProducer.java │ │ │ ├── SampleDecryptingConsumer.java │ │ │ └── AesGcmNoPaddingCryptoAlgorithm.java │ ├── index.adoc │ └── pom.xml ├── kafkastream-with-keyrepo-sample │ ├── runSamples.sh │ ├── docker-compose.yml │ ├── README.md │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── logback.xml │ │ │ └── java │ │ │ └── io │ │ │ └── quicksign │ │ │ └── kafka │ │ │ └── crypto │ │ │ └── samples │ │ │ └── stream │ │ │ └── keyrepo │ │ │ ├── SampleKeyNameExtractor.java │ │ │ ├── SampleKeyNameObfuscator.java │ │ │ ├── KeyStoreBasedKeyRepository.java │ │ │ ├── SamplesMain.java │ │ │ ├── AesGcmNoPaddingCryptoAlgorithm.java │ │ │ ├── SampleProducer.java │ │ │ └── SampleStream.java │ ├── generateKeys.sh │ ├── index.adoc │ └── pom.xml └── pom.xml ├── doc ├── running-docker-kafka.adoc └── pom.xml ├── keyrepository ├── src │ └── main │ │ └── java │ │ └── io │ │ └── quicksign │ │ └── kafka │ │ └── crypto │ │ └── keyrepository │ │ ├── KeyNameExtractor.java │ │ ├── KeyNameObfuscator.java │ │ ├── KeyRepository.java │ │ ├── RepositoryBasedKeyReferenceExtractor.java │ │ └── RepositoryBasedKeyProvider.java └── pom.xml ├── generatedkey ├── src │ └── main │ │ └── java │ │ └── io │ │ └── quicksign │ │ └── kafka │ │ └── crypto │ │ └── generatedkey │ │ ├── CryptoKeyGenerator.java │ │ ├── MasterKeyEncryption.java │ │ ├── PerRecordKeyProvider.java │ │ ├── AES256CryptoKeyGenerator.java │ │ └── KeyPerRecordKeyReferenceExtractor.java └── pom.xml ├── core ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── quicksign │ │ │ └── kafka │ │ │ └── crypto │ │ │ ├── encryption │ │ │ ├── KeyProvider.java │ │ │ ├── CryptoAlgorithm.java │ │ │ ├── DefaultEncryptor.java │ │ │ └── DefaultDecryptor.java │ │ │ ├── KafkaCryptoConstants.java │ │ │ ├── CryptoSerializerFactory.java │ │ │ ├── CryptoDeserializerFactory.java │ │ │ ├── utils │ │ │ └── ArrayUtils.java │ │ │ ├── Decryptor.java │ │ │ ├── Encryptor.java │ │ │ ├── pairing │ │ │ ├── serializer │ │ │ │ ├── SerializerPairFactory.java │ │ │ │ ├── SerializerPair.java │ │ │ │ └── CryptoSerializerPairFactory.java │ │ │ ├── serdes │ │ │ │ ├── SerdeFactory.java │ │ │ │ ├── SerdesPair.java │ │ │ │ └── CryptoSerdeFactory.java │ │ │ ├── keyextractor │ │ │ │ └── KeyReferenceExtractor.java │ │ │ ├── package-info.java │ │ │ └── internal │ │ │ │ └── CryptoAwareSerializerWrapper.java │ │ │ ├── CryptoSerializer.java │ │ │ └── CryptoDeserializer.java │ └── test │ │ └── java │ │ └── io │ │ └── quicksign │ │ └── kafka │ │ └── crypto │ │ ├── encryption │ │ ├── DefaultDecryptorTest.java │ │ └── DefaultEncryptorTest.java │ │ ├── CryptoDeserializerTest.java │ │ └── CryptoSerializerTest.java └── pom.xml ├── deploy ├── index.adoc ├── .travis.yml ├── README.md └── pom.xml /test: -------------------------------------------------------------------------------- 1 | echo "Running custom test script" 2 | 3 | mvn clean install -q -B 4 | -------------------------------------------------------------------------------- /.travis/gpg.asc.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickSign/kafka-encryption/HEAD/.travis/gpg.asc.enc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | target 4 | 5 | .classpath 6 | .project 7 | .settings 8 | .travis/gpg.asc 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Kafka Encryption 2 | Copyright 2018 Quicksign 3 | 4 | This product includes software developed at 5 | Quicksign (https://www.quicksign.com) 6 | 7 | This software has dependencies on 8 | Apache Kafka (https://kafka.apache.org/) 9 | -------------------------------------------------------------------------------- /HOW-TO-DEPLOY.md: -------------------------------------------------------------------------------- 1 | ## How to Deploy 2 | 3 | We pretty much do as explained here: 4 | 5 | https://bpodgursky.com/2019/07/31/using-travisci-to-deploy-to-maven-central-via-git-tagging-aka-death-to-commit-clutter/ 6 | 7 | Please note that we have added the --com option to all travis cli command 8 | -------------------------------------------------------------------------------- /.mvn/jgitver.config.xml: -------------------------------------------------------------------------------- 1 | 4 | true 5 | 6 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | fr.brouillard.oss 5 | jgitver-maven-plugin 6 | 1.3.0 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/README.md: -------------------------------------------------------------------------------- 1 | ### Embedded per message encryption key sample (Consumer API) 2 | 3 | In this example, a different encryption key is generated for each message, encrypted using a master key and stored 4 | embedded with the payload. 5 | 6 | #### Running the sample 7 | 8 | cd samples/kafka-encryption-generatedkey-sample 9 | mvn package 10 | docker-compose up # on windows and OSX, you need to adjust 11 | sh generateMasterKey.sh 12 | sh runSamples.sh 13 | -------------------------------------------------------------------------------- /doc/running-docker-kafka.adoc: -------------------------------------------------------------------------------- 1 | [TIP] 2 | ==== 3 | You need to have a running Kafka in order to run the examples, here's how you can using docker. 4 | 5 | *On OSX and Windows* 6 | 7 | [source,indent=0] 8 | .... 9 | docker run --rm -p 2181:2181 -p 3030:3030 -p 8081-8083:8081-8083 -p 9581-9585:9581-9585 -p 9092:9092 -e ADV_HOST=192.168.99.100 landoop/fast-data-dev:2.0.1 10 | .... 11 | 12 | *On linux* 13 | 14 | [source,indent=0] 15 | .... 16 | docker run --rm --net=host -e ADV_HOST=localhost landoop/fast-data-dev:2.0.1 17 | .... 18 | ==== 19 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/README.md: -------------------------------------------------------------------------------- 1 | ### Key repository sample (Consumer API) 2 | 3 | In this example, an encryption key reference is stored in the record payload. 4 | The encryption key is generated by (and stored in) the key repository. Two records with the same record's key embed the 5 | same encryption key reference. The key repository in this example is a simple in memory repository but it would be a 6 | key vault such as Google Cloud KMS or Hashicorp Vault in a real world example. 7 | 8 | #### Running the sample 9 | 10 | cd samples/kafka-encryption-keyrepo-sample 11 | mvn package 12 | docker-compose up # on windows and OSX, you need to adjust 13 | sh runSamples.sh 14 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/runSamples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Quicksign 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. See accompanying LICENSE file. 14 | 15 | java -jar target/kafka-encryption-keyrepo-sample-*-shaded.jar 16 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/runSamples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Quicksign 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. See accompanying LICENSE file. 14 | 15 | java -jar target/kafka-encryption-kafkastream-keyrepo-sample-*-shaded.jar 16 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Quicksign 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. See accompanying LICENSE file. 12 | 13 | version: '3' 14 | 15 | services: 16 | kafka: 17 | image: landoop/fast-data-dev:2.0.1 18 | network_mode: "host" 19 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Quicksign 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. See accompanying LICENSE file. 12 | 13 | version: '3' 14 | 15 | services: 16 | kafka: 17 | image: landoop/fast-data-dev:2.0.1 18 | network_mode: "host" 19 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/runSamples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Quicksign 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. See accompanying LICENSE file. 14 | 15 | java -jar target/kafka-encryption-generatedkey-sample-*-shaded.jar --producer --consumer --rawConsumer 16 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Quicksign 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. See accompanying LICENSE file. 12 | 13 | version: '3' 14 | 15 | services: 16 | kafka: 17 | image: landoop/fast-data-dev:2.0.1 18 | network_mode: "host" 19 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/README.md: -------------------------------------------------------------------------------- 1 | ### Key repository sample (Streams API) 2 | 3 | This is a dummy sample of a bank account. Each account has an associated 4 | key to encrypt all operations associated to the account. 5 | 6 | For this account, we manage the bank view and the agency view. 7 | 8 | * The bank has access to the full key repository (account 0 to 9). 9 | * Agency 1 has only access to a subset of key repository (account 0 to 4) 10 | * Agency 2 has only access to a subset of key repository (account 5 to 9) 11 | 12 | All operations are written encrypted into a common topic `operations` 13 | 14 | #### Running the sample 15 | 16 | cd samples/kafkastream-with-keyrepo-sample 17 | mvn clean package 18 | docker-compose up # on windows and OSX, you need to adjust 19 | sh generateKeys.sh 20 | sh runSamples.sh 21 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/generateMasterKey.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Quicksign 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. See accompanying LICENSE file. 14 | 15 | rm /tmp/sample.pkcs12 16 | keytool -genseckey -keystore /tmp/sample.pkcs12 -storetype pkcs12 -storepass sample -keyalg AES -keysize 256 -alias sample -keypass sample 17 | -------------------------------------------------------------------------------- /.travis/mvn-settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | ossrh 8 | ${env.OSSRH_USERNAME} 9 | ${env.OSSRH_PASSWORD} 10 | 11 | 12 | 13 | 14 | 15 | ossrh 16 | 17 | true 18 | 19 | 20 | gpg 21 | ${env.GPG_KEY_NAME} 22 | ${env.GPG_PASSPHRASE} 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /keyrepository/src/main/java/io/quicksign/kafka/crypto/keyrepository/KeyNameExtractor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.keyrepository; 21 | 22 | /** 23 | * Used at encryption time to infer key name from topic name and message key 24 | */ 25 | public interface KeyNameExtractor { 26 | 27 | String extractKeyName(String topic, Object key); 28 | } 29 | -------------------------------------------------------------------------------- /generatedkey/src/main/java/io/quicksign/kafka/crypto/generatedkey/CryptoKeyGenerator.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.generatedkey; 21 | 22 | /** 23 | * Generator for symetric encryption keys 24 | */ 25 | public interface CryptoKeyGenerator { 26 | 27 | /** 28 | * generate a symetric encryption key 29 | * 30 | * @return an encryption key 31 | */ 32 | byte[] generateKey(); 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/encryption/KeyProvider.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.encryption; 21 | 22 | import java.util.Optional; 23 | 24 | /** 25 | * A key provider has to provide the key associated to a keyRef. 26 | */ 27 | public interface KeyProvider { 28 | 29 | /** 30 | * @param keyRef the reference of the key to retrieve 31 | * @return the actual key or {@code Optional.empty()} if the key cannot be found 32 | */ 33 | Optional getKey(byte[] keyRef); 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/KafkaCryptoConstants.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | public class KafkaCryptoConstants { 23 | 24 | /** 25 | * header name to handle the key reference 26 | */ 27 | public static final String KEY_REF_HEADER = "keKeyReference"; 28 | 29 | /** 30 | * "magic" prefix added to all encrypted messages 31 | */ 32 | public static final byte[] ENCRYPTED_PREFIX = {0x2B, 0x45, 0x2B, 0x1B, 0x2B, 0x46}; 33 | 34 | private KafkaCryptoConstants() { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 25 | 26 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 25 | 26 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 25 | 26 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/CryptoSerializerFactory.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | import org.apache.kafka.common.serialization.Serializer; 23 | 24 | public class CryptoSerializerFactory { 25 | 26 | private final Encryptor encryptor; 27 | 28 | public CryptoSerializerFactory(Encryptor encryptor) { 29 | 30 | this.encryptor = encryptor; 31 | } 32 | 33 | public CryptoSerializer buildFrom(Serializer rawSerializer) { 34 | return new CryptoSerializer<>(rawSerializer, encryptor, null); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/index.adoc: -------------------------------------------------------------------------------- 1 | ### Embedded per message encryption key sample (Consumer API) 2 | 3 | In this example, a different encryption key is generated for each message, encrypted using a master key and stored 4 | embedded with the payload. 5 | 6 | #### Running the sample 7 | 8 | [source,indent=0] 9 | .... 10 | cd samples/kafka-encryption-generatedkey-sample 11 | docker-compose up # on windows and OSX, you need to adjust 12 | sh generateMasterKey.sh 13 | sh runSamples.sh 14 | .... 15 | 16 | include::../../doc/running-docker-kafka.adoc[] 17 | 18 | #### Usage 19 | 20 | ##### Producer side 21 | 22 | [source,java,indent=0] 23 | .... 24 | include::./src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/SamplesMain.java[tags=masterkey] 25 | .... 26 | 27 | [source,java,indent=0] 28 | .... 29 | include::./src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/SampleProducer.java[tags=produce] 30 | .... 31 | 32 | ##### Consumer side 33 | 34 | [source,java,indent=0] 35 | .... 36 | include::./src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/SamplesMain.java[tags=masterkey] 37 | .... 38 | 39 | [source,java,indent=0] 40 | .... 41 | include::./src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/SampleDecryptingConsumer.java[tags=consume] 42 | .... 43 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/CryptoDeserializerFactory.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | import org.apache.kafka.common.serialization.Deserializer; 23 | 24 | public class CryptoDeserializerFactory { 25 | 26 | private final Decryptor decryptor; 27 | 28 | public CryptoDeserializerFactory(Decryptor decryptor) { 29 | 30 | this.decryptor = decryptor; 31 | } 32 | 33 | public CryptoDeserializer buildFrom(Deserializer rawDeserializer) { 34 | return new CryptoDeserializer<>(rawDeserializer, decryptor); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/utils/ArrayUtils.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.utils; 21 | 22 | public class ArrayUtils { 23 | 24 | private ArrayUtils() { 25 | } 26 | 27 | public static boolean startWith(byte[] array, byte[] prefix) { 28 | if (array.length < prefix.length) { 29 | return false; 30 | } 31 | boolean isPrefix = true; 32 | for (int i = 0; i < prefix.length && isPrefix; i++) { 33 | isPrefix = isPrefix && (array[i] == prefix[i]); 34 | } 35 | return isPrefix; 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /keyrepository/src/main/java/io/quicksign/kafka/crypto/keyrepository/KeyNameObfuscator.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.keyrepository; 21 | 22 | /** 23 | *

Obfuscate the keyName to obtain the keyRef. Obfuscation has to be reversible.

24 | * 25 | *

Calling several time {@link #obfuscate(String)}

with the same keyName may give 26 | * different results. It is recommend to not let appear in the Kafka the same key reference 27 | * in several records

28 | */ 29 | public interface KeyNameObfuscator { 30 | 31 | byte[] obfuscate(String keyName); 32 | 33 | String unObfuscate(byte[] keyref); 34 | } 35 | -------------------------------------------------------------------------------- /keyrepository/src/main/java/io/quicksign/kafka/crypto/keyrepository/KeyRepository.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.keyrepository; 21 | 22 | import java.util.Optional; 23 | 24 | /** 25 | * It provides a key according to its name. 26 | * It can represent keys stored on a Vault. 27 | */ 28 | public interface KeyRepository { 29 | 30 | /** 31 | * It retrieves the key from the key repository. 32 | * It will return {@link Optional#EMPTY} if the key can not be found on the key repository 33 | * 34 | * @param keyName 35 | * @return 36 | */ 37 | Optional getKey(String keyName); 38 | } 39 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/index.adoc: -------------------------------------------------------------------------------- 1 | ### Key repository sample (Consumer API) 2 | 3 | In this example, an encryption key reference is stored in the record payload. 4 | The encryption key is generated by (and stored in) the key repository. Two records with the same record's key embed the 5 | same encryption key reference. The key repository in this example is a simple in memory repository but it would be a 6 | key vault such as Google Cloud KMS or Hashicorp Vault in a real world example. 7 | 8 | #### Running the sample 9 | 10 | [source,indent=0] 11 | .... 12 | cd samples/kafka-encryption-keyrepo-sample 13 | docker-compose up # on windows and OSX, you need to adjust 14 | sh runSamples.sh 15 | .... 16 | 17 | include::../../doc/running-docker-kafka.adoc[] 18 | 19 | #### Usage 20 | 21 | 22 | ##### Sample Main 23 | 24 | [source,java,indent=0] 25 | .... 26 | include::./src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SamplesMain.java[tags=main] 27 | .... 28 | 29 | ##### Producer side 30 | 31 | 32 | [source,java,indent=0] 33 | .... 34 | include::./src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SampleProducer.java[tags=produce] 35 | .... 36 | 37 | ##### Consumer side 38 | 39 | [source,java,indent=0] 40 | .... 41 | include::./src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SampleDecryptingConsumer.java[tags=consume] 42 | .... 43 | -------------------------------------------------------------------------------- /deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # expects variables to be set: 3 | # - OSSRH_USERNAME 4 | # - OSSRH_PASSWORD 5 | # - GPG_KEY_NAME 6 | # - GPG_PASSPHRASE 7 | # expects file to exist: 8 | # - .travis/gpg.asc 9 | 10 | set -e 11 | 12 | # Check the variables are set 13 | if [ -z "$OSSRH_USERNAME" ]; then 14 | echo "missing environment value: OSSRH_USERNAME" >&2 15 | exit 1 16 | fi 17 | 18 | if [ -z "$OSSRH_PASSWORD" ]; then 19 | echo "missing environment value: OSSRH_PASSWORD" >&2 20 | exit 1 21 | fi 22 | 23 | if [ -z "$GPG_KEY_NAME" ]; then 24 | echo "missing environment value: GPG_KEY_NAME" >&2 25 | exit 1 26 | fi 27 | 28 | if [ -z "$GPG_PASSPHRASE" ]; then 29 | echo "missing environment value: GPG_PASSPHRASE" >&2 30 | exit 1 31 | fi 32 | 33 | # Prepare the local keyring (requires travis to have decrypted the file 34 | # beforehand) 35 | gpg --fast-import .travis/gpg.asc 36 | 37 | if [ ! -z "$TRAVIS_TAG" ] 38 | then 39 | echo "on a tag -> set pom.xml to $TRAVIS_TAG" 40 | mvn --settings "${TRAVIS_BUILD_DIR}/.travis/mvn-settings.xml" org.codehaus.mojo:versions-maven-plugin:2.1:set -DnewVersion=$TRAVIS_TAG 1>/dev/null 2>/dev/null 41 | else 42 | echo "not on a tag -> keep snapshot version in pom.xml" 43 | fi 44 | 45 | # Run the maven deploy steps 46 | mvn deploy -P publish -DskipTests=true --settings "${TRAVIS_BUILD_DIR}/.travis/mvn-settings.xml" 47 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SampleKeyNameExtractor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.keyrepo; 21 | 22 | import io.quicksign.kafka.crypto.keyrepository.KeyNameExtractor; 23 | 24 | 25 | /** 26 | * Simple key name extractor that use the recordKey as the key name. 27 | * More complex implementation could leverage the key hidden structure 28 | * or take into account the current time. 29 | */ 30 | public class SampleKeyNameExtractor implements KeyNameExtractor { 31 | 32 | @Override 33 | public String extractKeyName(String topic, Object recordKey) { 34 | return recordKey.toString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SampleKeyNameExtractor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.stream.keyrepo; 21 | 22 | import io.quicksign.kafka.crypto.keyrepository.KeyNameExtractor; 23 | 24 | 25 | /** 26 | * Simple key name extractor that use the recordKey as the key name. 27 | * More complex implementation could leverage the key hidden structure 28 | * or take into account the current time. 29 | */ 30 | public class SampleKeyNameExtractor implements KeyNameExtractor { 31 | 32 | @Override 33 | public String extractKeyName(String topic, Object recordKey) { 34 | return "cpt" + recordKey; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /generatedkey/src/main/java/io/quicksign/kafka/crypto/generatedkey/MasterKeyEncryption.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.generatedkey; 21 | 22 | /** 23 | * Encryption to encrypt/decrypt generated keys 24 | */ 25 | public interface MasterKeyEncryption { 26 | 27 | /** 28 | * encrypt the provided key with a master key 29 | * 30 | * @param key the key to be encrypted 31 | * @return 32 | */ 33 | byte[] encryptKey(byte[] key); 34 | 35 | /** 36 | * decrypt the key with a master key 37 | * 38 | * @param encryptedKey 39 | * @return the decrypted key or {@code null} if the key can not be decrypted 40 | */ 41 | byte[] decryptKey(byte[] encryptedKey); 42 | } 43 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/Decryptor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | /** 23 | * Interface for decryption. It encapsulates the retriaval of the decryption key using the key reference 24 | * and the decryption of the data 25 | */ 26 | public interface Decryptor { 27 | 28 | /** 29 | * Decrypt the data. 30 | * It will return null if the key can not be retrieve or if decryption fails. 31 | * 32 | * @param data value to be decrypted 33 | * @param keyRef reference to the decryption key 34 | * @return the decrypted value or {@code null} if the key cannot be retrieved or if decryption fails 35 | */ 36 | byte[] decrypt(byte[] data, byte[] keyRef); 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/Encryptor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | /** 23 | * Interface for encryption. It encapsulates the retriaval of the encryption key using the key reference 24 | * and the encryption of the data 25 | */ 26 | public interface Encryptor { 27 | 28 | /** 29 | * Encrypt the data. 30 | * It will return null if encryption fails or if the key associated to the key reference 31 | * can not be found. 32 | * 33 | * @param value value to be encrypted 34 | * @param keyRef reference of the key 35 | * @return the encrypted value or {@code null} if the encryption fails 36 | */ 37 | byte[] encrypt(byte[] value, byte[] keyRef); 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/serializer/SerializerPairFactory.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.pairing.serializer; 21 | 22 | import org.apache.kafka.common.serialization.Serializer; 23 | 24 | /** 25 | * A factory to pair 2 {@link Serializer} 26 | *

27 | * Main usage is with traditional Kafka Producer 28 | */ 29 | public interface SerializerPairFactory { 30 | 31 | /** 32 | * Pair the keySerializer with the value serializer 33 | * 34 | * @param keySerializer 35 | * @param valueSerializer 36 | * @param 37 | * @param 38 | * @return 39 | */ 40 | SerializerPair build(Serializer keySerializer, Serializer valueSerializer); 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/serdes/SerdeFactory.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.pairing.serdes; 21 | 22 | import org.apache.kafka.common.serialization.Serde; 23 | 24 | /** 25 | * A factory to pair 2 {@link org.apache.kafka.common.serialization.Serde Serde} 26 | *

27 | * Main usage is for Kafka Streams 28 | */ 29 | public interface SerdeFactory { 30 | 31 | /** 32 | * Pair the keySerde with the valueSerde 33 | * 34 | * @param keySerde 35 | * @param valueSerde 36 | * @param 37 | * @param 38 | * @return 39 | */ 40 | SerdesPair buildSerdesPair(Serde keySerde, Serde valueSerde); 41 | 42 | Serde buildSelfCryptoAwareSerde(Serde valueSerde); 43 | } 44 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/generateKeys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Quicksign 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. See accompanying LICENSE file. 14 | 15 | rm /tmp/samplestream.pkcs12 16 | rm /tmp/samplestream1.pkcs12 17 | rm /tmp/samplestream2.pkcs12 18 | for i in `seq 0 9`; do 19 | keytool -genseckey -keystore /tmp/samplestream.pkcs12 -storetype pkcs12 -storepass sample -keyalg AES -keysize 256 -alias cpt$i -keypass sample 20 | done 21 | 22 | for i in `seq 0 4`; do 23 | keytool -importkeystore -srckeystore /tmp/samplestream.pkcs12 -srcstorepass sample \ 24 | -destkeystore /tmp/samplestream1.pkcs12 -deststoretype pkcs12 -deststorepass sample \ 25 | -srcalias cpt$i 26 | done 27 | 28 | for i in `seq 5 9`; do 29 | keytool -importkeystore -srckeystore /tmp/samplestream.pkcs12 -srcstorepass sample \ 30 | -destkeystore /tmp/samplestream2.pkcs12 -deststoretype pkcs12 -deststorepass sample \ 31 | -srcalias cpt$i 32 | done 33 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/keyextractor/KeyReferenceExtractor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.pairing.keyextractor; 21 | 22 | /** 23 | * Used at encryption time to compute the keyref associated to the message key. 24 | *

25 | * The choice of a {@link KeyReferenceExtractor} has to be done accordingly 26 | * to the chosen {@link io.quicksign.kafka.crypto.encryption.KeyProvider KeyProvider} 27 | * 28 | * @see io.quicksign.kafka.crypto.encryption.KeyProvider 29 | */ 30 | public interface KeyReferenceExtractor { 31 | 32 | /** 33 | * Compute the key reference for a message 34 | * 35 | * @param topic the topic name on which the message is about to be published 36 | * @param key the message key (in nominal case) 37 | * @return the keyref 38 | */ 39 | byte[] extractKeyReference(String topic, Object key); 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/serializer/SerializerPair.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.pairing.serializer; 21 | 22 | import org.apache.kafka.common.serialization.Serializer; 23 | 24 | /** 25 | * Represent paired {@link Serializer} 26 | * 27 | * @param 28 | * @param 29 | */ 30 | public class SerializerPair { 31 | 32 | private final Serializer keySerializer; 33 | private final Serializer valueSerializer; 34 | 35 | public SerializerPair(Serializer keySerializer, Serializer valueSerializer) { 36 | 37 | this.keySerializer = keySerializer; 38 | this.valueSerializer = valueSerializer; 39 | } 40 | 41 | public Serializer getKeySerializer() { 42 | return keySerializer; 43 | } 44 | 45 | public Serializer getValueSerializer() { 46 | return valueSerializer; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/encryption/CryptoAlgorithm.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.encryption; 21 | 22 | /** 23 | * Interface to handle cryptographic algorithm 24 | * 25 | * @see DefaultDecryptor 26 | * @see DefaultEncryptor 27 | */ 28 | public interface CryptoAlgorithm { 29 | 30 | /** 31 | * Encrypt the data using the provided key 32 | * 33 | * @param data message to be encrypted 34 | * @param key encryption key 35 | * @return encrypted message 36 | * @throws Exception 37 | */ 38 | byte[] encrypt(byte[] data, byte[] key) throws Exception; 39 | 40 | /** 41 | * Decrypt the data using the provided key 42 | * 43 | * @param encryptedData message to be decrypted 44 | * @param key encryption key 45 | * @return decrypted message 46 | * @throws Exception 47 | */ 48 | byte[] decrypt(byte[] encryptedData, byte[] key) throws Exception; 49 | } 50 | -------------------------------------------------------------------------------- /generatedkey/src/main/java/io/quicksign/kafka/crypto/generatedkey/PerRecordKeyProvider.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.generatedkey; 21 | 22 | import java.util.Optional; 23 | 24 | import io.quicksign.kafka.crypto.encryption.KeyProvider; 25 | 26 | /** 27 | * This key provider consider key reference as the result of the encryption by a master. 28 | *

29 | * It will try to decrypt the key using the master key. The result will be {@link Optional#EMPTY} if the key can not be decrypted 30 | */ 31 | public class PerRecordKeyProvider implements KeyProvider { 32 | 33 | private final MasterKeyEncryption masterKeyEncryption; 34 | 35 | public PerRecordKeyProvider(MasterKeyEncryption masterKeyEncryption) { 36 | 37 | this.masterKeyEncryption = masterKeyEncryption; 38 | } 39 | 40 | 41 | @Override 42 | public Optional getKey(byte[] keyRef) { 43 | return Optional.ofNullable(masterKeyEncryption.decryptKey(keyRef)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /index.adoc: -------------------------------------------------------------------------------- 1 | # Quicksign Kafka Encryption 2 | 3 | v{project-version} 4 | 5 | ## Motivation 6 | 7 | Personal data protection is becoming increasingly important for everyone these days. We chose no compromise, we encrypt 8 | everything end to end. 9 | 10 | Quicksign uses Kafka as the backbone of its microservices architecture, we therefore needed a solution 11 | to achieve end to end encryption in Kafka. Since it didn't appear to be available, we had to implement our own. We 12 | believe opening it to the community will help broaden best practices in terms of personal data protection and improve 13 | its security through a larger audience scrutiny. 14 | 15 | CAUTION: This library is the actual one used on our platform but some particular encryption and operational details 16 | are not revealed here and as such this library on its own doesn't reflect the overall mechanism used at Quicksign 17 | to protect our users data. 18 | 19 | ## Usage 20 | 21 | ### Import maven dependency 22 | 23 | [source,xml,subs="attributes+"] 24 | .... 25 | 26 | {project-groupId} 27 | kafka-encryption-core 28 | {project-version} 29 | 30 | .... 31 | 32 | ## Design goals 33 | 34 | * Support multiple encryption key management strategies (KMS, embedded, per message, rolling windows, per tenant, custom) 35 | * Support for Kafka Streams intermediate topics 36 | * Detect when a payload is not encrypted to skip decryption 37 | * Support for Camel 38 | * Support for Spring Boot 39 | 40 | ## Use cases 41 | include::./samples/keyrepo-sample/index.adoc[] 42 | include::./samples/generatedkey-sample/index.adoc[] 43 | include::./samples/kafkastream-with-keyrepo-sample/index.adoc[] 44 | -------------------------------------------------------------------------------- /generatedkey/src/main/java/io/quicksign/kafka/crypto/generatedkey/AES256CryptoKeyGenerator.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.generatedkey; 21 | 22 | import java.security.NoSuchAlgorithmException; 23 | 24 | import javax.crypto.KeyGenerator; 25 | import javax.crypto.SecretKey; 26 | 27 | /** 28 | * Generator for AES-256 encryption key 29 | */ 30 | public class AES256CryptoKeyGenerator implements CryptoKeyGenerator { 31 | 32 | /** 33 | * Generate a new AES-256 encryption key 34 | * 35 | * @return a new AES-256 encryption key 36 | */ 37 | @Override 38 | public byte[] generateKey() { 39 | try { 40 | KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); 41 | keyGenerator.init(256); 42 | SecretKey secretKey = keyGenerator.generateKey(); 43 | return secretKey.getEncoded(); 44 | } 45 | catch (NoSuchAlgorithmException e) { 46 | throw new RuntimeException("unable to handle AES encryption", e); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/index.adoc: -------------------------------------------------------------------------------- 1 | ### Key repository sample (Streams API) 2 | 3 | This is a dummy sample of a bank account. Each account has an associated 4 | key to encrypt all operations associated to the account. 5 | 6 | For this account, we manage the bank view and the agency view. 7 | 8 | * The bank has access to the full key repository (acount 0 to 9). 9 | * Agency 1 has only access to a subset of key repository (account 0 to 4) 10 | * Agency 2 has only access to a subset of key repostory (account 5 to 9) 11 | 12 | All operations are written encrypted into a common topic `operations` 13 | 14 | #### Running the sample 15 | 16 | [source,indent=0] 17 | .... 18 | cd samples/kafkastream-with-keyrepo-sample 19 | docker-compose up # on windows and OSX, you need to adjust 20 | sh generateKeys.sh 21 | sh runSamples.sh 22 | .... 23 | 24 | include::../../doc/running-docker-kafka.adoc[] 25 | 26 | #### Usage 27 | 28 | 29 | ##### Sample Main 30 | 31 | [source,java,indent=0] 32 | .... 33 | include::./src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SamplesMain.java[tags=main] 34 | .... 35 | 36 | ##### Producer side 37 | 38 | .SampleProducer.java 39 | [source,java,indent=0] 40 | .... 41 | include::./src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SampleProducer.java[tags=produce] 42 | .... 43 | 44 | ##### Stream side 45 | 46 | .SampleStream.java 47 | [source,java,indent=0] 48 | .... 49 | include::./src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SampleStream.java[tags=stream] 50 | .... 51 | 52 | 53 | and display 54 | 55 | .SampleStream.java 56 | [source,java,indent=0] 57 | .... 58 | include::./src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SampleStream.java[tags=display] 59 | .... 60 | -------------------------------------------------------------------------------- /generatedkey/src/main/java/io/quicksign/kafka/crypto/generatedkey/KeyPerRecordKeyReferenceExtractor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.generatedkey; 21 | 22 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 23 | 24 | /** 25 | * KeyReference extractor that will generate an new key for each record. 26 | * The key reference will be the key encrypted by the master key. 27 | */ 28 | public class KeyPerRecordKeyReferenceExtractor implements KeyReferenceExtractor { 29 | 30 | private final CryptoKeyGenerator cryptoKeyGenerator; 31 | private final MasterKeyEncryption masterKeyEncryption; 32 | 33 | public KeyPerRecordKeyReferenceExtractor(CryptoKeyGenerator cryptoKeyGenerator, 34 | MasterKeyEncryption masterKeyEncryption) { 35 | 36 | this.cryptoKeyGenerator = cryptoKeyGenerator; 37 | this.masterKeyEncryption = masterKeyEncryption; 38 | } 39 | 40 | 41 | @Override 42 | public byte[] extractKeyReference(String topic, Object key) { 43 | return masterKeyEncryption.encryptKey(cryptoKeyGenerator.generateKey()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 4.0.0 19 | 20 | 21 | io.quicksign 22 | kafka-encryption-parent 23 | 0.0.0-SNAPSHOT 24 | 25 | 26 | kafka-encryption-samples 27 | pom 28 | 29 | kafka-encryption-samples 30 | Kafka Record Encryption Samples 31 | 32 | 33 | generatedkey-sample 34 | keyrepo-sample 35 | kafkastream-with-keyrepo-sample 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-deploy-plugin 43 | 2.7 44 | 45 | true 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SampleKeyRepository.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.keyrepo; 21 | 22 | import java.util.Map; 23 | import java.util.Optional; 24 | import java.util.concurrent.ConcurrentHashMap; 25 | 26 | import io.quicksign.kafka.crypto.generatedkey.AES256CryptoKeyGenerator; 27 | import io.quicksign.kafka.crypto.generatedkey.CryptoKeyGenerator; 28 | import io.quicksign.kafka.crypto.keyrepository.KeyRepository; 29 | 30 | /** 31 | * In a real life scenario your encryption keys could be stored encrypted in a persistent storage accessible by 32 | * the producer and the consumer, you would of course cache them locally in memory to avoid introducing too much 33 | * latency. In our example, the repository is a simple map. 34 | */ 35 | public class SampleKeyRepository implements KeyRepository { 36 | 37 | // Use an AES256 key generator 38 | private CryptoKeyGenerator cryptoKeyGenerator = new AES256CryptoKeyGenerator(); 39 | private Map keyStore = new ConcurrentHashMap<>(); 40 | 41 | @Override 42 | public Optional getKey(String keyName) { 43 | return Optional.of(keyStore.computeIfAbsent(keyName, k -> cryptoKeyGenerator.generateKey())); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SampleKeyNameObfuscator.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.keyrepo; 21 | 22 | import java.nio.charset.Charset; 23 | import java.util.Arrays; 24 | 25 | import io.quicksign.kafka.crypto.keyrepository.KeyNameObfuscator; 26 | 27 | 28 | /** 29 | * In the payload we store the encryption key reference which is simply an obfuscated version fo the keyName. 30 | * This simple implementation creates the encryption key reference by swapping 2 bytes from the encryption key name. 31 | */ 32 | public class SampleKeyNameObfuscator implements KeyNameObfuscator { 33 | @Override 34 | public byte[] obfuscate(String keyName) { 35 | return swapFirstAndLast(keyName.getBytes(Charset.forName("UTF-8"))); 36 | } 37 | 38 | @Override 39 | public String unObfuscate(byte[] keyref) { 40 | byte[] copy = Arrays.copyOf(keyref, keyref.length); 41 | return new String(swapFirstAndLast(copy), Charset.forName("UTF-8")); 42 | } 43 | 44 | private byte[] swapFirstAndLast(byte[] keyRef) { 45 | byte[] copy = Arrays.copyOf(keyRef, keyRef.length); 46 | byte first = copy[0]; 47 | copy[0] = copy[copy.length - 1]; 48 | copy[copy.length - 1] = first; 49 | return copy; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SampleKeyNameObfuscator.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.stream.keyrepo; 21 | 22 | import java.nio.charset.Charset; 23 | import java.util.Arrays; 24 | 25 | import io.quicksign.kafka.crypto.keyrepository.KeyNameObfuscator; 26 | 27 | 28 | /** 29 | * In the payload we store the encryption key reference which is simply an obfuscated version fo the keyName. 30 | * This simple implementation creates the encryption key reference by swapping 2 bytes from the encryption key name. 31 | */ 32 | public class SampleKeyNameObfuscator implements KeyNameObfuscator { 33 | @Override 34 | public byte[] obfuscate(String keyName) { 35 | return swapFirstAndLast(keyName.getBytes(Charset.forName("UTF-8"))); 36 | } 37 | 38 | @Override 39 | public String unObfuscate(byte[] keyref) { 40 | byte[] copy = Arrays.copyOf(keyref, keyref.length); 41 | return new String(swapFirstAndLast(copy), Charset.forName("UTF-8")); 42 | } 43 | 44 | private byte[] swapFirstAndLast(byte[] keyRef) { 45 | byte[] copy = Arrays.copyOf(keyRef, keyRef.length); 46 | byte first = copy[0]; 47 | copy[0] = copy[copy.length - 1]; 48 | copy[copy.length - 1] = first; 49 | return copy; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /keyrepository/src/main/java/io/quicksign/kafka/crypto/keyrepository/RepositoryBasedKeyReferenceExtractor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.keyrepository; 21 | 22 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 23 | 24 | 25 | /** 26 | * KeyReference used when the key to use is stored on a repository, and the key to use can be computed from the record key 27 | * 28 | * @see KeyNameExtractor 29 | * @see KeyNameObfuscator 30 | */ 31 | public class RepositoryBasedKeyReferenceExtractor implements KeyReferenceExtractor { 32 | 33 | private final KeyNameExtractor keyNameExtractor; 34 | private final KeyNameObfuscator keyNameObfuscator; 35 | 36 | /** 37 | * @param keyNameExtractor used to compute the keyName 38 | * @param keyNameObfuscator used to obfuscate the keyName 39 | */ 40 | public RepositoryBasedKeyReferenceExtractor(KeyNameExtractor keyNameExtractor, KeyNameObfuscator keyNameObfuscator) { 41 | 42 | this.keyNameExtractor = keyNameExtractor; 43 | this.keyNameObfuscator = keyNameObfuscator; 44 | } 45 | 46 | 47 | @Override 48 | public byte[] extractKeyReference(String topic, Object key) { 49 | String keyName = keyNameExtractor.extractKeyName(topic, key); 50 | return keyName == null ? null : keyNameObfuscator.obfuscate(keyName); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /keyrepository/src/main/java/io/quicksign/kafka/crypto/keyrepository/RepositoryBasedKeyProvider.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.keyrepository; 21 | 22 | import java.util.Optional; 23 | 24 | import io.quicksign.kafka.crypto.encryption.KeyProvider; 25 | 26 | /** 27 | * KeyProvider based on a key repository 28 | * 29 | * @see KeyRepository 30 | * @see KeyNameObfuscator 31 | */ 32 | public class RepositoryBasedKeyProvider implements KeyProvider { 33 | 34 | private final KeyRepository keyRepository; 35 | private final KeyNameObfuscator keyNameObfuscator; 36 | 37 | /** 38 | * @param keyRepository the repository for retrieving keys 39 | * @param keyNameObfuscator KeyNameObfuscator used to unobfuscate the keyName 40 | */ 41 | public RepositoryBasedKeyProvider(KeyRepository keyRepository, KeyNameObfuscator keyNameObfuscator) { 42 | this.keyRepository = keyRepository; 43 | this.keyNameObfuscator = keyNameObfuscator; 44 | } 45 | 46 | /** 47 | * Retrieve the key on the key repository. It will first unobfucate the keyRef to obtain the keyName. 48 | * 49 | * @param keyRef the reference of the key to retrieve 50 | * @return 51 | */ 52 | @Override 53 | public Optional getKey(byte[] keyRef) { 54 | return keyRepository.getKey(keyNameObfuscator.unObfuscate(keyRef)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/test/java/io/quicksign/kafka/crypto/encryption/DefaultDecryptorTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.encryption; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | import static org.mockito.BDDMockito.given; 24 | 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.Optional; 27 | 28 | import org.junit.Test; 29 | import org.junit.runner.RunWith; 30 | import org.mockito.InjectMocks; 31 | import org.mockito.Mock; 32 | import org.mockito.runners.MockitoJUnitRunner; 33 | 34 | @RunWith(MockitoJUnitRunner.class) 35 | public class DefaultDecryptorTest { 36 | 37 | @Mock 38 | KeyProvider keyProvider; 39 | 40 | @Mock 41 | CryptoAlgorithm encryptionAlgorithm; 42 | 43 | @InjectMocks 44 | DefaultDecryptor defaultDecryptor; 45 | 46 | @Test 47 | public void testDecrypt() throws Exception { 48 | byte[] keyRef = "keyref".getBytes(StandardCharsets.UTF_8); 49 | byte[] key = "key".getBytes(StandardCharsets.UTF_8); 50 | byte[] clearData = "clear data".getBytes(StandardCharsets.UTF_8); 51 | byte[] encodedData = "encoded data".getBytes(StandardCharsets.UTF_8); 52 | 53 | given(keyProvider.getKey(keyRef)).willReturn(Optional.of(key)); 54 | given(encryptionAlgorithm.decrypt(encodedData, key)).willReturn(clearData); 55 | 56 | byte[] res = defaultDecryptor.decrypt(encodedData, keyRef); 57 | assertThat(res).isEqualTo(clearData); 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/test/java/io/quicksign/kafka/crypto/encryption/DefaultEncryptorTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.encryption; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | import static org.mockito.BDDMockito.given; 24 | 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.Optional; 27 | 28 | import org.junit.Test; 29 | import org.junit.runner.RunWith; 30 | import org.mockito.InjectMocks; 31 | import org.mockito.Mock; 32 | import org.mockito.runners.MockitoJUnitRunner; 33 | 34 | @RunWith(MockitoJUnitRunner.class) 35 | public class DefaultEncryptorTest { 36 | 37 | @Mock 38 | KeyProvider keyProvider; 39 | 40 | @Mock 41 | CryptoAlgorithm encryptionAlgorithm; 42 | 43 | @InjectMocks 44 | DefaultEncryptor defaultEncryptor; 45 | 46 | @Test 47 | public void testEncrypt() throws Exception { 48 | byte[] keyRef = "keyref".getBytes(StandardCharsets.UTF_8); 49 | byte[] key = "key".getBytes(StandardCharsets.UTF_8); 50 | byte[] clearData = "clear data".getBytes(StandardCharsets.UTF_8); 51 | byte[] encodedData = "encoded data".getBytes(StandardCharsets.UTF_8); 52 | 53 | given(keyProvider.getKey(keyRef)).willReturn(Optional.of(key)); 54 | given(encryptionAlgorithm.encrypt(clearData, key)).willReturn(encodedData); 55 | 56 | byte[] res = defaultEncryptor.encrypt(clearData, keyRef); 57 | 58 | assertThat(res).isEqualTo(encodedData); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/encryption/DefaultEncryptor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.encryption; 21 | 22 | import java.util.Optional; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import io.quicksign.kafka.crypto.Encryptor; 28 | 29 | /** 30 | * Default implementation of Encryptor. 31 | * It uses a {@link KeyProvider} to retrieve the key associated to keyr references. 32 | * It use a {@link CryptoAlgorithm} to encrypt the data 33 | */ 34 | public class DefaultEncryptor implements Encryptor { 35 | 36 | private static final Logger log = LoggerFactory.getLogger(DefaultEncryptor.class); 37 | 38 | private final KeyProvider keyProvider; 39 | private final CryptoAlgorithm cryptoAlgorithm; 40 | 41 | public DefaultEncryptor(KeyProvider keyProvider, CryptoAlgorithm cryptoAlgorithm) { 42 | 43 | this.keyProvider = keyProvider; 44 | this.cryptoAlgorithm = cryptoAlgorithm; 45 | } 46 | 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | @Override 52 | public byte[] encrypt(byte[] value, byte[] keyRef) { 53 | //error on key retrieving must stop the world 54 | Optional maybeKey = keyProvider.getKey(keyRef); 55 | return maybeKey.map(key -> { 56 | try { 57 | return cryptoAlgorithm.encrypt(value, key); 58 | } 59 | catch (Exception e) { 60 | log.error("error while encrypting data", e); 61 | return null; 62 | } 63 | }).orElse(null); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/package-info.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | /** 21 | *

A common use case is to choose the encryption key accordingly to the record key. 22 | * For example, in an event log, you may want to encrypt all records relative to the 23 | * same transaction with the same encryption key and the transaction id can be deducted 24 | * from the record key.

25 | * 26 | *

To acheive this, the key serializer and the value serializer will be paired. 27 | * The key serializer will be wrapped to call the a 28 | * {@link io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor KeyReferenceExtractor} 29 | * and put in the context the cryptographic key reference, that will be used to encrypt the record value.

30 | * 31 | *

This based on the fact that {@link org.apache.kafka.clients.producer.KafkaProducer#doSend(org.apache.kafka.clients.producer.ProducerRecord, org.apache.kafka.clients.producer.Callback) KafkaProducer} 32 | * and {@link org.apache.kafka.streams.processor.internals.RecordCollectorImpl#send(java.lang.String, java.lang.Object, java.lang.Object, org.apache.kafka.common.header.Headers, java.lang.Integer, java.lang.Long, org.apache.kafka.common.serialization.Serializer, org.apache.kafka.common.serialization.Serializer) RecordCollector} 33 | * (for streams) 34 | * call key serialization before value serialization. 35 | *

36 | * 37 | * @see io.quicksign.kafka.crypto.pairing.serializer.CryptoSerializerPairFactory 38 | * @see io.quicksign.kafka.crypto.pairing.serdes.CryptoSerdeFactory 39 | */ 40 | package io.quicksign.kafka.crypto.pairing; 41 | 42 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/encryption/DefaultDecryptor.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.encryption; 21 | 22 | import java.util.Optional; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import io.quicksign.kafka.crypto.Decryptor; 28 | 29 | /** 30 | * Default implementation of Decryptor. 31 | * It uses a {@link KeyProvider} to retrieve the key associated to keyr references. 32 | * It use a {@link CryptoAlgorithm} to decrypt the data 33 | */ 34 | public class DefaultDecryptor implements Decryptor { 35 | 36 | private static final Logger log = LoggerFactory.getLogger(DefaultDecryptor.class); 37 | 38 | private final KeyProvider keyProvider; 39 | private final CryptoAlgorithm cryptoAlgorithm; 40 | 41 | public DefaultDecryptor(KeyProvider keyProvider, CryptoAlgorithm cryptoAlgorithm) { 42 | 43 | this.keyProvider = keyProvider; 44 | this.cryptoAlgorithm = cryptoAlgorithm; 45 | } 46 | 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | @Override 52 | public byte[] decrypt(byte[] value, byte[] keyRef) { 53 | //error on key retrieving must stop the world 54 | Optional maybeKey = keyProvider.getKey(keyRef); 55 | return maybeKey.map(key -> { 56 | try { 57 | return cryptoAlgorithm.decrypt(value, key); 58 | } 59 | catch (Exception e) { 60 | log.error("error while decrypting data", e); 61 | return null; 62 | } 63 | } 64 | ).orElse(null); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/serializer/CryptoSerializerPairFactory.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.pairing.serializer; 21 | 22 | import org.apache.kafka.common.serialization.Serializer; 23 | 24 | import io.quicksign.kafka.crypto.CryptoSerializer; 25 | import io.quicksign.kafka.crypto.Encryptor; 26 | import io.quicksign.kafka.crypto.pairing.internal.CryptoAwareSerializerWrapper; 27 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 28 | 29 | /** 30 | * A factory to pair 2 serializers 31 | *
    32 | *
  • the keySerializer is wrapped to call the {@link KeyReferenceExtractor}
  • 33 | *
  • the valueSerializer is wrapped into a {@link CryptoSerializer}
  • 34 | *
35 | * The keyref extracted by the wrapped key serializer will be shared with the wrapped value serializer using Kafka headers 36 | */ 37 | public class CryptoSerializerPairFactory implements SerializerPairFactory { 38 | 39 | private final Encryptor encryptor; 40 | private final KeyReferenceExtractor keyReferenceExtractor; 41 | 42 | public CryptoSerializerPairFactory(Encryptor encryptor, KeyReferenceExtractor keyReferenceExtractor) { 43 | this.encryptor = encryptor; 44 | this.keyReferenceExtractor = keyReferenceExtractor; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | @Override 51 | public SerializerPair build(Serializer keySerializer, Serializer valueSerializer) { 52 | Serializer newKeySerializer = new CryptoAwareSerializerWrapper(keySerializer, keyReferenceExtractor, null); 53 | Serializer newvalueSerializer = new CryptoSerializer<>(valueSerializer, encryptor, null); 54 | return new SerializerPair<>(newKeySerializer, newvalueSerializer); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /generatedkey/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 4.0.0 19 | 20 | 21 | io.quicksign 22 | kafka-encryption-parent 23 | 0.0.0-SNAPSHOT 24 | 25 | 26 | kafka-encryption-generatedkey 27 | jar 28 | 29 | kafka-encryption-generatedkey 30 | Kafka Record Encryption - generated keys 31 | 32 | 33 | 34 | 35 | io.quicksign 36 | kafka-encryption-core 37 | 38 | 39 | 40 | org.slf4j 41 | slf4j-api 42 | 43 | 44 | 45 | org.mockito 46 | mockito-core 47 | 1.10.19 48 | test 49 | 50 | 51 | 52 | junit 53 | junit 54 | 4.13.1 55 | test 56 | 57 | 58 | 59 | org.assertj 60 | assertj-core 61 | 3.6.2 62 | test 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /keyrepository/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 4.0.0 19 | 20 | 21 | io.quicksign 22 | kafka-encryption-parent 23 | 0.0.0-SNAPSHOT 24 | 25 | 26 | kafka-encryption-keyrepository 27 | jar 28 | 29 | kafka-encryption-keyrepository 30 | Kafka Record Encryption - key repository 31 | 32 | 33 | 34 | 35 | io.quicksign 36 | kafka-encryption-core 37 | 38 | 39 | 40 | org.slf4j 41 | slf4j-api 42 | 43 | 44 | 45 | org.mockito 46 | mockito-core 47 | 1.10.19 48 | test 49 | 50 | 51 | 52 | junit 53 | junit 54 | 4.13.1 55 | test 56 | 57 | 58 | 59 | org.assertj 60 | assertj-core 61 | 3.6.2 62 | test 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 4.0.0 19 | 20 | 21 | io.quicksign 22 | kafka-encryption-parent 23 | 0.0.0-SNAPSHOT 24 | 25 | 26 | kafka-encryption-core 27 | jar 28 | 29 | kafka-encryption-core 30 | Kafka Record Encryption 31 | 32 | 33 | 34 | 35 | org.slf4j 36 | slf4j-api 37 | 38 | 39 | 40 | org.apache.kafka 41 | kafka-clients 42 | 43 | 44 | 45 | org.apache.kafka 46 | kafka-streams 47 | 48 | 49 | 50 | org.mockito 51 | mockito-core 52 | 1.10.19 53 | test 54 | 55 | 56 | 57 | junit 58 | junit 59 | 4.13.1 60 | test 61 | 62 | 63 | 64 | org.assertj 65 | assertj-core 66 | 3.6.2 67 | test 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/KeyStoreBasedMasterKey.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.generatedkey; 21 | 22 | import java.io.File; 23 | import java.io.FileInputStream; 24 | import java.io.InputStream; 25 | import java.security.Key; 26 | import java.security.KeyStore; 27 | 28 | import io.quicksign.kafka.crypto.generatedkey.MasterKeyEncryption; 29 | 30 | public class KeyStoreBasedMasterKey implements MasterKeyEncryption { 31 | 32 | private final Key masterKey; 33 | private final AesGcmNoPaddingCryptoAlgorithm aesGcmNoPaddingCryptoAlgorithm; 34 | 35 | 36 | public KeyStoreBasedMasterKey(File masterKeyFile, String masterKeyPass, String alias, AesGcmNoPaddingCryptoAlgorithm aesGcmNoPaddingCryptoAlgorithm) { 37 | this.aesGcmNoPaddingCryptoAlgorithm = aesGcmNoPaddingCryptoAlgorithm; 38 | try (InputStream keystoreStream = new FileInputStream(masterKeyFile)) { 39 | KeyStore keystore = KeyStore.getInstance("PKCS12"); 40 | keystore.load(keystoreStream, masterKeyPass.toCharArray()); 41 | if (!keystore.containsAlias(alias)) { 42 | throw new RuntimeException("Alias for key not found"); 43 | } 44 | masterKey = keystore.getKey(alias, masterKeyPass.toCharArray()); 45 | 46 | } 47 | catch (Exception e) { 48 | throw new RuntimeException(e); 49 | } 50 | } 51 | 52 | 53 | @Override 54 | public byte[] encryptKey(byte[] key) { 55 | try { 56 | return aesGcmNoPaddingCryptoAlgorithm.encrypt(key, masterKey.getEncoded()); 57 | } 58 | catch (Exception e) { 59 | throw new RuntimeException(e); 60 | } 61 | } 62 | 63 | @Override 64 | public byte[] decryptKey(byte[] encryptedKey) { 65 | try { 66 | return aesGcmNoPaddingCryptoAlgorithm.decrypt(encryptedKey, masterKey.getEncoded()); 67 | } 68 | catch (Exception e) { 69 | return null; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/KeyStoreBasedKeyRepository.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.stream.keyrepo; 21 | 22 | import java.io.File; 23 | import java.io.FileInputStream; 24 | import java.io.InputStream; 25 | import java.security.Key; 26 | import java.security.KeyStore; 27 | import java.util.Optional; 28 | import java.util.concurrent.ConcurrentHashMap; 29 | import java.util.concurrent.ConcurrentMap; 30 | 31 | import io.quicksign.kafka.crypto.keyrepository.KeyRepository; 32 | 33 | public class KeyStoreBasedKeyRepository implements KeyRepository { 34 | 35 | private final KeyStore keyStore; 36 | private final ConcurrentMap> keyCache = new ConcurrentHashMap<>(); 37 | private final String keyPass; 38 | 39 | public KeyStoreBasedKeyRepository(File masterKeyFile, String keyPass) { 40 | this.keyPass = keyPass; 41 | try (InputStream keystoreStream = new FileInputStream(masterKeyFile)) { 42 | KeyStore keystore = KeyStore.getInstance("PKCS12"); 43 | keystore.load(keystoreStream, keyPass.toCharArray()); 44 | 45 | this.keyStore = keystore; 46 | } 47 | catch (Exception e) { 48 | throw new RuntimeException(e); 49 | } 50 | } 51 | 52 | 53 | @Override 54 | public Optional getKey(String keyName) { 55 | Optional maybeKey = keyCache.computeIfAbsent(keyName, this::loadKey); 56 | 57 | return maybeKey.map(Key::getEncoded); 58 | } 59 | 60 | private synchronized Optional loadKey(String keyName) { 61 | try { 62 | if (keyStore.containsAlias(keyName)) { 63 | return Optional.ofNullable(keyStore.getKey(keyName, keyPass.toCharArray())); 64 | } 65 | else { 66 | return Optional.empty(); 67 | } 68 | } 69 | catch (Exception e) { 70 | throw new RuntimeException("error while loading key", e); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SampleRawConsumer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.keyrepo; 21 | 22 | import java.util.Collections; 23 | import java.util.Properties; 24 | 25 | import org.apache.kafka.clients.consumer.ConsumerConfig; 26 | import org.apache.kafka.clients.consumer.ConsumerRecords; 27 | import org.apache.kafka.clients.consumer.KafkaConsumer; 28 | import org.apache.kafka.common.serialization.LongDeserializer; 29 | import org.apache.kafka.common.serialization.StringDeserializer; 30 | 31 | // This consumer does not decrypt the messages... it is here to show you that the data 32 | // is encrypted... 33 | public class SampleRawConsumer implements Runnable { 34 | 35 | public SampleRawConsumer() { 36 | } 37 | 38 | @Override 39 | public void run() { 40 | 41 | Properties consumerProperties = new Properties(); 42 | consumerProperties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 43 | consumerProperties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "sampleraw"); 44 | consumerProperties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); 45 | 46 | try (KafkaConsumer consumer = new KafkaConsumer( 47 | consumerProperties, 48 | new LongDeserializer(), 49 | new StringDeserializer())) { 50 | 51 | consumer.subscribe(Collections.singleton("sampletopic")); 52 | for (; true; ) { 53 | ConsumerRecords records = consumer.poll(1000L); 54 | records.forEach( 55 | record -> System.out.println( 56 | "-------------------------------------------------------------\n" + 57 | "raw record: key=" + record.key() + ", offset=" + record.offset() + ", value=" + record.value() + 58 | "\n-------------------------------------------------------------\n\n") 59 | ); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SamplesMain.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.keyrepo; 21 | 22 | import java.util.concurrent.ExecutorService; 23 | import java.util.concurrent.Executors; 24 | 25 | import io.quicksign.kafka.crypto.encryption.CryptoAlgorithm; 26 | import io.quicksign.kafka.crypto.encryption.KeyProvider; 27 | import io.quicksign.kafka.crypto.keyrepository.RepositoryBasedKeyProvider; 28 | import io.quicksign.kafka.crypto.keyrepository.RepositoryBasedKeyReferenceExtractor; 29 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 30 | 31 | public class SamplesMain { 32 | 33 | 34 | public static void main(String... args) { 35 | 36 | // tag::main[] 37 | 38 | // 1- SETUP 39 | 40 | // key provider backed by a simple in memory key repository 41 | KeyProvider keyProvider = new RepositoryBasedKeyProvider(new SampleKeyRepository(), new SampleKeyNameObfuscator()); 42 | 43 | // We create an encryption keyref for each record's key. 44 | // Two records with the same record key have the same encryption key ref. 45 | KeyReferenceExtractor keyReferenceExtractor = new RepositoryBasedKeyReferenceExtractor(new SampleKeyNameExtractor(), new SampleKeyNameObfuscator()); 46 | 47 | // The payload is encrypted using AES 48 | CryptoAlgorithm cryptoAlgorithm = new AesGcmNoPaddingCryptoAlgorithm(); 49 | 50 | 51 | // 2- RUN 52 | 53 | ExecutorService executorService = Executors.newFixedThreadPool(3); 54 | 55 | // the producer encrypts the message 56 | executorService.submit(new SampleProducer(keyProvider, keyReferenceExtractor, cryptoAlgorithm)); 57 | 58 | // this consumer reads them but don't decrypt them... so you can see that it can't be read by default 59 | executorService.submit(new SampleRawConsumer()); 60 | 61 | // this consumer reads and decrypts... and dump in clear the payload... 62 | executorService.submit(new SampleDecryptingConsumer(keyProvider, cryptoAlgorithm)); 63 | 64 | // end::main[] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/SampleRawConsumer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.generatedkey; 21 | 22 | import java.util.Collections; 23 | import java.util.Properties; 24 | 25 | import org.apache.kafka.clients.consumer.ConsumerConfig; 26 | import org.apache.kafka.clients.consumer.ConsumerRecords; 27 | import org.apache.kafka.clients.consumer.KafkaConsumer; 28 | import org.apache.kafka.common.serialization.LongDeserializer; 29 | import org.apache.kafka.common.serialization.StringDeserializer; 30 | 31 | // This consumer does not decrypt the messages... it is here to show you that the data 32 | // is encrypted... 33 | public class SampleRawConsumer implements Runnable { 34 | 35 | public SampleRawConsumer() { 36 | } 37 | 38 | 39 | @Override 40 | public void run() { 41 | 42 | Properties consumerProperties = new Properties(); 43 | consumerProperties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 44 | consumerProperties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "sampleraw"); 45 | consumerProperties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); 46 | 47 | try (KafkaConsumer consumer = new KafkaConsumer( 48 | consumerProperties, 49 | new LongDeserializer(), 50 | new StringDeserializer())) { 51 | 52 | consumer.subscribe(Collections.singleton("sampletopic")); 53 | for (; true; ) { 54 | ConsumerRecords records = consumer.poll(1000L); 55 | records.forEach( 56 | record -> System.out.println( 57 | "-------------------------------------------------------------\n" + 58 | "raw record: key=" + record.key() + ", offset=" + record.offset() + ", value=" + record.value() + 59 | "\n-------------------------------------------------------------\n\n" 60 | ) 61 | ); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/SamplesMain.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.generatedkey; 21 | 22 | import java.io.File; 23 | import java.util.Arrays; 24 | import java.util.Optional; 25 | import java.util.Set; 26 | import java.util.concurrent.ExecutorService; 27 | import java.util.concurrent.Executors; 28 | import java.util.function.Function; 29 | import java.util.stream.Collectors; 30 | 31 | import io.quicksign.kafka.crypto.generatedkey.MasterKeyEncryption; 32 | 33 | public class SamplesMain { 34 | 35 | public static void main(String... args) { 36 | Set modules = Arrays.stream(args) 37 | .map(Modules::getByOption) 38 | .filter(Optional::isPresent) 39 | .map(Optional::get) 40 | .collect(Collectors.toSet()); 41 | 42 | // tag::masterkey[] 43 | MasterKeyEncryption masterKeyEncryption = new KeyStoreBasedMasterKey( 44 | new File("/tmp/sample.pkcs12"), 45 | "sample", "sample", 46 | new AesGcmNoPaddingCryptoAlgorithm() 47 | ); 48 | // end::masterkey[] 49 | 50 | 51 | ExecutorService executorService = Executors.newFixedThreadPool(3); 52 | 53 | for (Modules activeModule : modules) { 54 | Runnable task = activeModule.getBuilder().apply(masterKeyEncryption); 55 | executorService.submit(task); 56 | } 57 | } 58 | 59 | private enum Modules { 60 | PRODUCER("--producer", mk -> new SampleProducer(mk)), 61 | CONSUMER("--consumer", mk -> new SampleDecryptingConsumer(mk)), 62 | RAW_CONSUMER("--rawConsumer", mk -> new SampleRawConsumer()); 63 | 64 | private final String option; 65 | private final Function builder; 66 | 67 | Modules(String option, Function builder) { 68 | this.option = option; 69 | this.builder = builder; 70 | } 71 | 72 | public static Optional getByOption(String option) { 73 | return Arrays.stream(values()).filter(m -> m.option.equals(option)).findFirst(); 74 | } 75 | 76 | public Function getBuilder() { 77 | return builder; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: "/bin/true" 3 | script: 4 | - "./test" 5 | env: 6 | global: 7 | - secure: ug5U2l40AI2F3gpzo6YN+pWFrzxCayb6PSjH2n5fn4kr6s6BDdNHYjUHp+mtsTSvXGzBNJcxIcmuulWFsTV1iSPJaYX5Ke7TjgI8A4pSTytNnuxVOLTENc9T8+uKgq0n0BPPH0ru2o90D8pqIz+6xlg956AUvTqkZVP94ydhT1Z89Pq+LGJVTRLnOXMPvYl9XqtusGB/oVUXIjuzqMS1FHcTru352Zn53dgd4h+5g8ikh5a/qiygRbpiyBPvjXz//ft0iN8dr7iOaxAFoS93nNqxnHpJ058T49bMhD6V82BraYnZG8wlDxe+kRaFbBWC24rsP9Aov1jcA8x/q50MC2BTQ8B8I2QEd+bxkhcPgqmNKfDY5fP75tPeYb9ZBC1zD9XM1U/lmJd5isYTw2uxkV+uhBxYOhXDY2g3BbOwD5a/7YiRJd2/UGdMJHmYC+m09Cr4SPzNjkoJZBdRaGUPMeAwUvU5mI4E+yW7N21INuPFXX4GIAKBxC4QKOt5bAbW0D3shYKBcjJ+HUUz0b6XyLHmRD1v7MPRUWQITJWEOuH5/lhut4MKkUxjv0b1O6HMXrBpYoEZ3seIKqXaRlONMQoYhwWHTABPG9GpZrVzKRyTXWORgbppAVveJd88M8asiR7vZbx8u3dQEJ/m6vC7Qu9U1lM0ZdI/ZwYY432r1J4= 8 | - secure: Y+d6V70Kv3QuamZ6AVKKs4tcvFCnBwCzcWISsTCwoa9aiVV3lItubi4xhX6w+7x2XInSYuWSEDiMDIPgtuQthSJ66749gMuwzNB+JOMdX6ou01Fgz3Ng4JvKcAkCMaUlm40pg8SlbyLpigyjrF4yyq+KsJrX8n6OxW7MdH3+6LFztUdmIEVnFchb4xYcJ/Kuj8Z9Qv6++QeDdGeuhCy0SKskAq3KnViNcjKez1+El0OMLwYCrAcjca+9/AfuMcG4hBDpsvxvX5hHsllyMq7HZnrGidEqGNjNMAOKRfNslPtg0NFYv3xUZxP9NEylHCe25lc5tmrpXWEZYn9a79uuvw3SAl1uCuZwMHvijNf04y3eJQH2PKuaa98EesjzEmHY/m8Q4ciXS6h0Wi4HzWLr/l9onJvA0c2ibKgfZ0LOmZ1cQdHYt9VsFEognvLiLFDzPsJ/gSzNKOWRqa7VwdwI6eskEf+TknXfGXhwjz5M03lZVyZBHFlrY2SxGBnv1qf+xotU7jF6owj3/clVxPy0bJFVQZDUVOPi6xe+OFp954q6DtWast+HnEdztUcghw4gdJ1v57WTbp8ShdNc53VyUUSa805tYyKcmhUuKwPdcUNqWZgRTxa6+OQF4PUf6ED0G5ClY9bJUDnJRJgq1MgW0Vb3PG0iDeEVitKb8wP7gww= 9 | - secure: NKp7vfUdgVMzjmOUva+8uuSt/p5PRxHm72OfhcTPvgBoo1KqZcvF8vU5HlxCau3lwQs9H33K3SC+NG1jl2QJtW4gRprRBig5piEHsbEuIYtscNWpszaIJH7hlNW/dUiXil6Ml1Nn2ZAYjxuBKIa/YMIjmFYK5O2dXfx1qFRgmUFlTXWtInLoLwTtBu6FIlN7sdo0V5tooZTYHQj5krTLf14X2ztNyUjM+xX/yCFstt8xVXv/Ubf9RYKupANtJUUyAvKl6KI6T+hcPbdd96BVdLbLLV8aBSF6+GQctfs9xIRycXvUYXhiu992A5Ix/H5npyD5Fc9xParmnTCU//D6/gYTLHl0tpbl7imjTcKWlfcqZTTLrUId+m19NUJQ/05WkmPnALO4uxq8ZiFyLFtEFzd1QttKM8OR/qhJcqnJm1wGsNogRxDIADID+fLfV7B+6YOU7HgBvKtoDq/LVCccYUDG8vlXfLPs29egzpWGcrxaEGfRqUyfgrTW8wxjROHIEPk+blTl4OqshRWlVlfH+ctShTgAPh682fEooXSqM1xm8VRY5dVBLX0OHMJsjgjCqrc88PcY2Ndt9lqQlLW8JvzglHdzHEcYDJZzH41SHbsT+ah2NJASBEuBVRJo4kC/ol2xPmbC7cM9bUOmCF/cB5v3DmUhnVbVFGxsjOpIypo= 10 | - secure: CdSu+A26H4ritU8FqgKpqHkMR+rYTBH2eMBAmGtbuN4T0X10bJBGLGAHsKXdLne+YoiLEe75AhzCmh36Bw6hQ0J2c6MQL7T25rmb7iGaNeDM3U0BxCM/iZKFMNevqxX8fqWNkc3vx7UZkqP/eXxr5kQYZdNRGxuVTKrHrbARZdW6pLfvAbCJMO88f8irOsYi1T8qG+LJjW3lkNCq4vkf0AX9EN4Az5HK7yIZMgbpYa+3wNgmCyfFEn7rT+LMl0ZFhugKx0VVnOuzEyz9w05jlu91XvnkQiohoMbqQATr73yan99OeJLy84cd2tQFz0iqaE1DHW4ZzBLh0l0PUEmrCn3KwKnsOBkjKMDg2nwkU1kMjo7HwXoMzdwCS11aCyhaASv8Cef+vY7c41UvMSvI/Lpq6KmFmPzm1Wd7tZ01BJJf/Q6Ot6wIYk0R2NHAydMYtPFvbz3ydNDvMHePUKXROP1rvbpfjoCAzVNn7aiXNOI+mT6Iy/qiIlkJgo/BG+80QIPx4XHoitvVnxwQ46G+gfmRVZMqpgybnJPn/cKAOeCsqBI/RxeTSs3FbXAEEXKhnTygF2NGk7q14oQJ+xooT3mm9RAiKiLkRmctzSYSyPTg9/wVlp845e7ciEgGC+tyLcOb52geKTre8xjInTHDcrLUoFjUzj1Mtu3z3HJ/mcs= 11 | before_deploy: 12 | - openssl aes-256-cbc -K $encrypted_4f0d00631887_key -iv $encrypted_4f0d00631887_iv 13 | -in .travis/gpg.asc.enc -out .travis/gpg.asc -d 14 | deploy: 15 | - skip_cleanup: true 16 | provider: script 17 | script: "./deploy" 18 | on: 19 | branch: master 20 | - skip_cleanup: true 21 | provider: script 22 | script: "./deploy" 23 | on: 24 | tags: true 25 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/serdes/SerdesPair.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.pairing.serdes; 21 | 22 | import org.apache.kafka.common.serialization.Serde; 23 | import org.apache.kafka.streams.kstream.Consumed; 24 | import org.apache.kafka.streams.kstream.Grouped; 25 | import org.apache.kafka.streams.kstream.Materialized; 26 | import org.apache.kafka.streams.kstream.Produced; 27 | import org.apache.kafka.streams.kstream.Serialized; 28 | import org.apache.kafka.streams.processor.StateStore; 29 | 30 | /** 31 | * Represent paired keySerde and valueSerde 32 | * 33 | * @param 34 | * @param 35 | */ 36 | public class SerdesPair { 37 | 38 | private final Serde keySerde; 39 | private final Serde valueSerde; 40 | 41 | public SerdesPair(Serde keySerde, Serde valueSerde) { 42 | 43 | this.keySerde = keySerde; 44 | this.valueSerde = valueSerde; 45 | } 46 | 47 | public Serde getKeySerde() { 48 | return keySerde; 49 | } 50 | 51 | public Serde getValueSerde() { 52 | return valueSerde; 53 | } 54 | 55 | /** 56 | * Build a {@link Serialized} using the keySerde and valueSerde of the pair 57 | * 58 | * @return 59 | */ 60 | public Serialized toSerialized() { 61 | return Serialized.with(keySerde, valueSerde); 62 | } 63 | 64 | /** 65 | * Build a {@link Grouped} using the keySerde and valueSerde of the pair 66 | */ 67 | public Grouped toGrouped() { 68 | return Grouped.with(keySerde, valueSerde); 69 | } 70 | 71 | /** 72 | * Build a {@link Produced} using the keySerde and valueSerde of the pair 73 | * 74 | * @return 75 | */ 76 | public Produced toProduced() { 77 | return Produced.with(keySerde, valueSerde); 78 | } 79 | 80 | /** 81 | * Build a {@link Consumed} using the keySerde and valueSerde of the pair 82 | * 83 | * @return 84 | */ 85 | public Consumed toConsumed() { 86 | return Consumed.with(keySerde, valueSerde); 87 | } 88 | 89 | 90 | /** 91 | * Apply the keySerde and valueSerde of the pair to a {@link Materialized} 92 | * 93 | * @param materialized 94 | * @param 95 | * @return 96 | */ 97 | public Materialized applyTo(Materialized materialized) { 98 | return materialized.withKeySerde(keySerde).withValueSerde(valueSerde); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SampleProducer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.keyrepo; 21 | 22 | import java.util.Properties; 23 | 24 | import org.apache.kafka.clients.producer.KafkaProducer; 25 | import org.apache.kafka.clients.producer.ProducerConfig; 26 | import org.apache.kafka.clients.producer.ProducerRecord; 27 | import org.apache.kafka.common.serialization.LongSerializer; 28 | import org.apache.kafka.common.serialization.StringSerializer; 29 | 30 | import io.quicksign.kafka.crypto.Encryptor; 31 | import io.quicksign.kafka.crypto.encryption.CryptoAlgorithm; 32 | import io.quicksign.kafka.crypto.encryption.DefaultEncryptor; 33 | import io.quicksign.kafka.crypto.encryption.KeyProvider; 34 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 35 | import io.quicksign.kafka.crypto.pairing.serializer.CryptoSerializerPairFactory; 36 | import io.quicksign.kafka.crypto.pairing.serializer.SerializerPair; 37 | 38 | public class SampleProducer implements Runnable { 39 | 40 | private final KeyProvider keyProvider; 41 | private final KeyReferenceExtractor keyReferenceExtractor; 42 | private final CryptoAlgorithm cryptoAlgorithm; 43 | 44 | public SampleProducer(KeyProvider keyProvider, KeyReferenceExtractor keyReferenceExtractor, CryptoAlgorithm cryptoAlgorithm) { 45 | this.keyProvider = keyProvider; 46 | this.keyReferenceExtractor = keyReferenceExtractor; 47 | this.cryptoAlgorithm = cryptoAlgorithm; 48 | } 49 | 50 | @Override 51 | public void run() { 52 | 53 | // tag::produce[] 54 | 55 | Encryptor encryptor = new DefaultEncryptor(keyProvider, cryptoAlgorithm); 56 | 57 | // Wrap base LongSerializer and StringSerializer with encrypted wrappers 58 | CryptoSerializerPairFactory cryptoSerializerPairFactory = new CryptoSerializerPairFactory(encryptor, keyReferenceExtractor); 59 | SerializerPair serializerPair = cryptoSerializerPairFactory.build(new LongSerializer(), new StringSerializer()); 60 | 61 | Properties producerProperties = new Properties(); 62 | producerProperties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 63 | 64 | try (KafkaProducer producer = 65 | new KafkaProducer<>(producerProperties, serializerPair.getKeySerializer(), serializerPair.getValueSerializer())) { 66 | 67 | for (long i = 0L; i < Long.MAX_VALUE; i++) { 68 | producer.send(new ProducerRecord<>("sampletopic", i, "test number " + i)); 69 | try { 70 | Thread.sleep(1000L); 71 | } 72 | catch (InterruptedException e) { 73 | return; 74 | } 75 | } 76 | } 77 | // end::produce[] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/SampleDecryptingConsumer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.keyrepo; 21 | 22 | import java.util.Collections; 23 | import java.util.Properties; 24 | 25 | import org.apache.kafka.clients.consumer.ConsumerConfig; 26 | import org.apache.kafka.clients.consumer.ConsumerRecords; 27 | import org.apache.kafka.clients.consumer.KafkaConsumer; 28 | import org.apache.kafka.common.serialization.LongDeserializer; 29 | import org.apache.kafka.common.serialization.StringDeserializer; 30 | 31 | import io.quicksign.kafka.crypto.CryptoDeserializerFactory; 32 | import io.quicksign.kafka.crypto.Decryptor; 33 | import io.quicksign.kafka.crypto.encryption.CryptoAlgorithm; 34 | import io.quicksign.kafka.crypto.encryption.DefaultDecryptor; 35 | import io.quicksign.kafka.crypto.encryption.KeyProvider; 36 | 37 | public class SampleDecryptingConsumer implements Runnable { 38 | 39 | private final KeyProvider keyProvider; 40 | private final CryptoAlgorithm cryptoAlgorithm; 41 | 42 | public SampleDecryptingConsumer(KeyProvider keyProvider, CryptoAlgorithm cryptoAlgorithm) { 43 | this.keyProvider = keyProvider; 44 | this.cryptoAlgorithm = cryptoAlgorithm; 45 | 46 | } 47 | 48 | @Override 49 | public void run() { 50 | 51 | // tag::consume[] 52 | // The key is embedded in each message 53 | 54 | Decryptor decryptor = new DefaultDecryptor(keyProvider, cryptoAlgorithm); 55 | 56 | // Construct decrypting deserializer 57 | CryptoDeserializerFactory cryptoDeserializerFactory = new CryptoDeserializerFactory(decryptor); 58 | 59 | Properties consumerProperties = new Properties(); 60 | consumerProperties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 61 | consumerProperties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "samplecrypted"); 62 | consumerProperties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); 63 | 64 | try (KafkaConsumer consumer = new KafkaConsumer( 65 | consumerProperties, 66 | new LongDeserializer(), 67 | cryptoDeserializerFactory.buildFrom(new StringDeserializer()))) { 68 | 69 | consumer.subscribe(Collections.singleton("sampletopic")); 70 | for (; true; ) { 71 | ConsumerRecords records = consumer.poll(1000L); 72 | records.forEach( 73 | record -> System.out.println( 74 | "-------------------------------------------------------------\n" + 75 | "decrypted record: key=" + record.key() + ", offset=" + record.offset() + ", value=" + record.value() + 76 | "\n-------------------------------------------------------------\n\n") 77 | ); 78 | } 79 | } 80 | // end::consume[] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SamplesMain.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.stream.keyrepo; 21 | 22 | import java.io.File; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Executors; 25 | 26 | import io.quicksign.kafka.crypto.encryption.KeyProvider; 27 | import io.quicksign.kafka.crypto.keyrepository.KeyRepository; 28 | import io.quicksign.kafka.crypto.keyrepository.RepositoryBasedKeyProvider; 29 | import io.quicksign.kafka.crypto.keyrepository.RepositoryBasedKeyReferenceExtractor; 30 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 31 | 32 | public class SamplesMain { 33 | 34 | public static void main(String... args) { 35 | 36 | // tag::main[] 37 | KeyRepository fullKeyRepository = new KeyStoreBasedKeyRepository( 38 | new File("/tmp/samplestream.pkcs12"), 39 | "sample" 40 | ); 41 | 42 | KeyRepository agency1KeyRepository = new KeyStoreBasedKeyRepository( 43 | new File("/tmp/samplestream1.pkcs12"), 44 | "sample" 45 | ); 46 | 47 | KeyRepository agency2KeyRepository = new KeyStoreBasedKeyRepository( 48 | new File("/tmp/samplestream2.pkcs12"), 49 | "sample" 50 | ); 51 | 52 | KeyProvider fullKeyProvider = new RepositoryBasedKeyProvider(fullKeyRepository, new SampleKeyNameObfuscator()); 53 | KeyProvider agency1KeyProvider = new RepositoryBasedKeyProvider(agency1KeyRepository, new SampleKeyNameObfuscator()); 54 | KeyProvider agency2KeyProvider = new RepositoryBasedKeyProvider(agency2KeyRepository, new SampleKeyNameObfuscator()); 55 | 56 | // We create an encryption keyref for each record's key. 57 | // Two records with the same record key have the same encryption key ref. 58 | KeyReferenceExtractor keyReferenceExtractor = new RepositoryBasedKeyReferenceExtractor(new SampleKeyNameExtractor(), new SampleKeyNameObfuscator()); 59 | 60 | 61 | ExecutorService executorService = Executors.newFixedThreadPool(4); 62 | 63 | 64 | SampleProducer sampleProducer = new SampleProducer(fullKeyProvider, keyReferenceExtractor); 65 | 66 | executorService.submit(sampleProducer); 67 | 68 | try { 69 | Thread.sleep(10000l); 70 | } 71 | catch (InterruptedException e) { 72 | System.exit(0); 73 | } 74 | 75 | SampleStream fullView = new SampleStream("full", fullKeyProvider, keyReferenceExtractor); 76 | SampleStream agency1View = new SampleStream("agency1", agency1KeyProvider, keyReferenceExtractor); 77 | SampleStream agency2View = new SampleStream("agency2", agency2KeyProvider, keyReferenceExtractor); 78 | 79 | executorService.submit(fullView); 80 | executorService.submit(agency1View); 81 | executorService.submit(agency2View); 82 | 83 | // end::main[] 84 | 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 4.0.0 19 | 20 | io.quicksign 21 | kafka-encryption-generatedkey-sample 22 | 0.0.0-SNAPSHOT 23 | jar 24 | kafka-encryption-generatedkey-sample 25 | Kafka Record Encryption - generated keys sample 26 | 27 | 28 | 1.8 29 | 1.8 30 | 1.8 31 | UTF-8 32 | 33 | 0.2.0 34 | 35 | 36 | 37 | 38 | 39 | io.quicksign 40 | kafka-encryption-core 41 | ${kafka-encryption.version} 42 | 43 | 44 | 45 | io.quicksign 46 | kafka-encryption-generatedkey 47 | ${kafka-encryption.version} 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-shade-plugin 57 | 3.2.0 58 | 59 | 60 | package 61 | 62 | shade 63 | 64 | 65 | true 66 | 67 | 68 | io.quicksign.kafka.crypto.samples.generatedkey.SamplesMain 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-deploy-plugin 78 | 2.7 79 | 80 | true 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/keyrepo/AesGcmNoPaddingCryptoAlgorithm.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.keyrepo; 21 | 22 | import java.io.ByteArrayOutputStream; 23 | import java.nio.ByteBuffer; 24 | import java.nio.charset.StandardCharsets; 25 | import java.security.NoSuchAlgorithmException; 26 | import java.security.SecureRandom; 27 | 28 | import javax.crypto.Cipher; 29 | import javax.crypto.CipherOutputStream; 30 | import javax.crypto.spec.GCMParameterSpec; 31 | import javax.crypto.spec.SecretKeySpec; 32 | 33 | import io.quicksign.kafka.crypto.encryption.CryptoAlgorithm; 34 | 35 | public class AesGcmNoPaddingCryptoAlgorithm implements CryptoAlgorithm { 36 | 37 | private static final String KEY_SPEC = "AES"; 38 | private static final String ALGO_TRANSFORMATION_STRING = "AES/GCM/NoPadding"; 39 | private static int IV_SIZE = 96; 40 | private static int TAG_BIT_LENGTH = 128; 41 | private static String TAG = "sample"; 42 | 43 | 44 | private final SecureRandom secureRandom; 45 | 46 | public AesGcmNoPaddingCryptoAlgorithm() { 47 | try { 48 | this.secureRandom = SecureRandom.getInstance("SHA1PRNG"); 49 | } 50 | catch (NoSuchAlgorithmException e) { 51 | throw new RuntimeException("unable to init secureRandom", e); 52 | } 53 | } 54 | 55 | @Override 56 | public byte[] encrypt(byte[] data, byte[] key) throws Exception { 57 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, KEY_SPEC); 58 | byte[] iv = new byte[IV_SIZE]; 59 | secureRandom.nextBytes(iv); 60 | GCMParameterSpec gcmParamSpec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); 61 | Cipher cipher = Cipher.getInstance(ALGO_TRANSFORMATION_STRING); 62 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParamSpec, secureRandom); 63 | cipher.updateAAD(TAG.getBytes(StandardCharsets.UTF_8)); 64 | 65 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 66 | baos.write(iv); 67 | 68 | try (CipherOutputStream cipherOutputStream = new CipherOutputStream(baos, cipher)) { 69 | cipherOutputStream.write(data); 70 | } 71 | 72 | return baos.toByteArray(); 73 | } 74 | 75 | @Override 76 | public byte[] decrypt(byte[] encryptedData, byte[] key) throws Exception { 77 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, KEY_SPEC); 78 | ByteBuffer byteBuffer = ByteBuffer.wrap(encryptedData); 79 | byte[] iv = new byte[IV_SIZE]; 80 | byteBuffer.get(iv); 81 | GCMParameterSpec gcmParamSpec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); 82 | Cipher cipher = Cipher.getInstance(ALGO_TRANSFORMATION_STRING); 83 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParamSpec, secureRandom); 84 | cipher.updateAAD(TAG.getBytes(StandardCharsets.UTF_8)); 85 | 86 | byte[] encryptedPayload = new byte[byteBuffer.remaining()]; 87 | byteBuffer.get(encryptedPayload); 88 | 89 | return cipher.doFinal(encryptedPayload); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/internal/CryptoAwareSerializerWrapper.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.pairing.internal; 21 | 22 | import java.util.Map; 23 | 24 | import org.apache.kafka.common.header.Headers; 25 | import org.apache.kafka.common.serialization.Serializer; 26 | 27 | import io.quicksign.kafka.crypto.KafkaCryptoConstants; 28 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 29 | 30 | /** 31 | * A wrapper for {@link Serializer}. 32 | * It will call the {@link KeyReferenceExtractor} and then serialize the data using the underlying serializer 33 | * 34 | * @param 35 | */ 36 | public class CryptoAwareSerializerWrapper implements Serializer { 37 | 38 | private final Serializer rawSerializer; 39 | private final KeyReferenceExtractor keyReferenceExtractor; 40 | private final ThreadLocal keyRefHolder; 41 | 42 | /** 43 | * @param rawSerializer the Serializer to use 44 | * @param keyReferenceExtractor the KeyReferenceExtractor to use 45 | * @param keyRefHolder the ThreadLocal to share the keyref (only used in the context of a Kafka Stream) 46 | */ 47 | public CryptoAwareSerializerWrapper(Serializer rawSerializer, KeyReferenceExtractor keyReferenceExtractor, ThreadLocal keyRefHolder) { 48 | this.rawSerializer = rawSerializer; 49 | this.keyReferenceExtractor = keyReferenceExtractor; 50 | this.keyRefHolder = keyRefHolder; 51 | } 52 | 53 | @Override 54 | public void configure(Map configs, boolean isKey) { 55 | this.rawSerializer.configure(configs, isKey); 56 | } 57 | 58 | /** 59 | * Call the KeyReferenceExtractor with the topic and the data and set the computed value into the ThreadLocal reference holder. 60 | *

61 | * This method is called in the context of a kafka stream and not in the Kafka Producer 62 | * 63 | * @param topic 64 | * @param data 65 | * @return the result of the underlying serializer 66 | */ 67 | @Override 68 | public byte[] serialize(String topic, T data) { 69 | this.keyRefHolder.set(keyReferenceExtractor.extractKeyReference(topic, data)); 70 | 71 | return this.rawSerializer.serialize(topic, data); 72 | } 73 | 74 | @Override 75 | public void close() { 76 | this.rawSerializer.close(); 77 | } 78 | 79 | /** 80 | * Call the KeyReferenceExtractor with the topic and the data and set the result in the kafka header {@link KafkaCryptoConstants#KEY_REF_HEADER} 81 | *

82 | * This method is called by the Kafka Producer 83 | * 84 | * @param topic 85 | * @param headers 86 | * @param data 87 | * @return the result of the underlying serializer 88 | */ 89 | @Override 90 | public byte[] serialize(String topic, Headers headers, T data) { 91 | headers.add(KafkaCryptoConstants.KEY_REF_HEADER, keyReferenceExtractor.extractKeyReference(topic, data)); 92 | return this.rawSerializer.serialize(topic, headers, data); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/AesGcmNoPaddingCryptoAlgorithm.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.generatedkey; 21 | 22 | import java.io.ByteArrayOutputStream; 23 | import java.nio.ByteBuffer; 24 | import java.nio.charset.StandardCharsets; 25 | import java.security.NoSuchAlgorithmException; 26 | import java.security.SecureRandom; 27 | 28 | import javax.crypto.Cipher; 29 | import javax.crypto.CipherOutputStream; 30 | import javax.crypto.spec.GCMParameterSpec; 31 | import javax.crypto.spec.SecretKeySpec; 32 | 33 | import io.quicksign.kafka.crypto.encryption.CryptoAlgorithm; 34 | 35 | public class AesGcmNoPaddingCryptoAlgorithm implements CryptoAlgorithm { 36 | 37 | private static final String KEY_SPEC = "AES"; 38 | private static final String ALGO_TRANSFORMATION_STRING = "AES/GCM/NoPadding"; 39 | private static int IV_SIZE = 96; 40 | private static int TAG_BIT_LENGTH = 128; 41 | private static String TAG = "sample"; 42 | 43 | 44 | private final SecureRandom secureRandom; 45 | 46 | public AesGcmNoPaddingCryptoAlgorithm() { 47 | try { 48 | this.secureRandom = SecureRandom.getInstance("SHA1PRNG"); 49 | } 50 | catch (NoSuchAlgorithmException e) { 51 | throw new RuntimeException("unable to init secureRandom", e); 52 | } 53 | } 54 | 55 | @Override 56 | public byte[] encrypt(byte[] data, byte[] key) throws Exception { 57 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, KEY_SPEC); 58 | byte[] iv = new byte[IV_SIZE]; 59 | secureRandom.nextBytes(iv); 60 | GCMParameterSpec gcmParamSpec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); 61 | Cipher cipher = Cipher.getInstance(ALGO_TRANSFORMATION_STRING); 62 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParamSpec, secureRandom); 63 | cipher.updateAAD(TAG.getBytes(StandardCharsets.UTF_8)); 64 | 65 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 66 | baos.write(iv); 67 | 68 | try (CipherOutputStream cipherOutputStream = new CipherOutputStream(baos, cipher)) { 69 | cipherOutputStream.write(data); 70 | } 71 | 72 | return baos.toByteArray(); 73 | } 74 | 75 | @Override 76 | public byte[] decrypt(byte[] encryptedData, byte[] key) throws Exception { 77 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, KEY_SPEC); 78 | ByteBuffer byteBuffer = ByteBuffer.wrap(encryptedData); 79 | byte[] iv = new byte[IV_SIZE]; 80 | byteBuffer.get(iv); 81 | GCMParameterSpec gcmParamSpec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); 82 | Cipher cipher = Cipher.getInstance(ALGO_TRANSFORMATION_STRING); 83 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParamSpec, secureRandom); 84 | cipher.updateAAD(TAG.getBytes(StandardCharsets.UTF_8)); 85 | 86 | byte[] encryptedPayload = new byte[byteBuffer.remaining()]; 87 | byteBuffer.get(encryptedPayload); 88 | 89 | return cipher.doFinal(encryptedPayload); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 4.0.0 19 | 20 | io.quicksign 21 | kafka-encryption-kafkastream-keyrepo-sample 22 | 0.0.0-SNAPSHOT 23 | jar 24 | kafka-encryption-kafkastream-keyrepo-sample 25 | Kafka Record Encryption - stream with key repository sample 26 | 27 | 28 | 1.8 29 | 1.8 30 | 1.8 31 | UTF-8 32 | 33 | 0.2.0 34 | 35 | 36 | 37 | 38 | 39 | io.quicksign 40 | kafka-encryption-core 41 | ${kafka-encryption.version} 42 | 43 | 44 | 45 | io.quicksign 46 | kafka-encryption-keyrepository 47 | ${kafka-encryption.version} 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-shade-plugin 57 | 3.2.0 58 | 59 | 60 | package 61 | 62 | shade 63 | 64 | 65 | true 66 | 67 | 68 | io.quicksign.kafka.crypto.samples.stream.keyrepo.SamplesMain 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-deploy-plugin 78 | 2.7 79 | 80 | true 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/AesGcmNoPaddingCryptoAlgorithm.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.stream.keyrepo; 21 | 22 | import java.io.ByteArrayOutputStream; 23 | import java.nio.ByteBuffer; 24 | import java.nio.charset.StandardCharsets; 25 | import java.security.NoSuchAlgorithmException; 26 | import java.security.SecureRandom; 27 | 28 | import javax.crypto.Cipher; 29 | import javax.crypto.CipherOutputStream; 30 | import javax.crypto.spec.GCMParameterSpec; 31 | import javax.crypto.spec.SecretKeySpec; 32 | 33 | import io.quicksign.kafka.crypto.encryption.CryptoAlgorithm; 34 | 35 | public class AesGcmNoPaddingCryptoAlgorithm implements CryptoAlgorithm { 36 | 37 | private static final String KEY_SPEC = "AES"; 38 | private static final String ALGO_TRANSFORMATION_STRING = "AES/GCM/NoPadding"; 39 | private static int IV_SIZE = 96; 40 | private static int TAG_BIT_LENGTH = 128; 41 | private static String TAG = "sample"; 42 | 43 | 44 | private final SecureRandom secureRandom; 45 | 46 | public AesGcmNoPaddingCryptoAlgorithm() { 47 | try { 48 | this.secureRandom = SecureRandom.getInstance("SHA1PRNG"); 49 | } 50 | catch (NoSuchAlgorithmException e) { 51 | throw new RuntimeException("unable to init secureRandom", e); 52 | } 53 | } 54 | 55 | @Override 56 | public byte[] encrypt(byte[] data, byte[] key) throws Exception { 57 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, KEY_SPEC); 58 | byte[] iv = new byte[IV_SIZE]; 59 | secureRandom.nextBytes(iv); 60 | GCMParameterSpec gcmParamSpec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); 61 | Cipher cipher = Cipher.getInstance(ALGO_TRANSFORMATION_STRING); 62 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParamSpec, secureRandom); 63 | cipher.updateAAD(TAG.getBytes(StandardCharsets.UTF_8)); 64 | 65 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 66 | baos.write(iv); 67 | 68 | try (CipherOutputStream cipherOutputStream = new CipherOutputStream(baos, cipher)) { 69 | cipherOutputStream.write(data); 70 | } 71 | 72 | return baos.toByteArray(); 73 | } 74 | 75 | @Override 76 | public byte[] decrypt(byte[] encryptedData, byte[] key) throws Exception { 77 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, KEY_SPEC); 78 | ByteBuffer byteBuffer = ByteBuffer.wrap(encryptedData); 79 | byte[] iv = new byte[IV_SIZE]; 80 | byteBuffer.get(iv); 81 | GCMParameterSpec gcmParamSpec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); 82 | Cipher cipher = Cipher.getInstance(ALGO_TRANSFORMATION_STRING); 83 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParamSpec, secureRandom); 84 | cipher.updateAAD(TAG.getBytes(StandardCharsets.UTF_8)); 85 | 86 | byte[] encryptedPayload = new byte[byteBuffer.remaining()]; 87 | byteBuffer.get(encryptedPayload); 88 | 89 | return cipher.doFinal(encryptedPayload); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SampleProducer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.stream.keyrepo; 21 | 22 | import java.util.Properties; 23 | import java.util.Random; 24 | 25 | import org.apache.kafka.clients.producer.KafkaProducer; 26 | import org.apache.kafka.clients.producer.ProducerConfig; 27 | import org.apache.kafka.clients.producer.ProducerRecord; 28 | import org.apache.kafka.common.serialization.IntegerSerializer; 29 | import org.apache.kafka.common.serialization.StringSerializer; 30 | 31 | import io.quicksign.kafka.crypto.Encryptor; 32 | import io.quicksign.kafka.crypto.encryption.DefaultEncryptor; 33 | import io.quicksign.kafka.crypto.encryption.KeyProvider; 34 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 35 | import io.quicksign.kafka.crypto.pairing.serializer.CryptoSerializerPairFactory; 36 | import io.quicksign.kafka.crypto.pairing.serializer.SerializerPair; 37 | 38 | public class SampleProducer implements Runnable { 39 | 40 | 41 | private final KeyProvider keyProvider; 42 | private final KeyReferenceExtractor keyReferenceExtractor; 43 | 44 | public SampleProducer(KeyProvider keyProvider, KeyReferenceExtractor keyReferenceExtractor) { 45 | this.keyProvider = keyProvider; 46 | this.keyReferenceExtractor = keyReferenceExtractor; 47 | } 48 | 49 | @Override 50 | public void run() { 51 | 52 | // tag::produce[] 53 | 54 | // The payload is encrypted using AES 55 | AesGcmNoPaddingCryptoAlgorithm cryptoAlgorithm = new AesGcmNoPaddingCryptoAlgorithm(); 56 | Encryptor encryptor = new DefaultEncryptor(keyProvider, cryptoAlgorithm); 57 | 58 | // Wrap base LongSerializer and StringSerializer with encrypted wrappers 59 | CryptoSerializerPairFactory cryptoSerializerPairFactory = new CryptoSerializerPairFactory(encryptor, 60 | keyReferenceExtractor); 61 | SerializerPair serializerPair = cryptoSerializerPairFactory.build(new IntegerSerializer(), new StringSerializer()); 62 | 63 | Properties producerProperties = new Properties(); 64 | producerProperties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 65 | 66 | Random random = new Random(); 67 | 68 | try (KafkaProducer producer = 69 | new KafkaProducer<>(producerProperties, serializerPair.getKeySerializer(), serializerPair.getValueSerializer())) { 70 | 71 | for (long i = 0L; i < Long.MAX_VALUE; i++) { 72 | long accountId = i % 10l; 73 | producer.send(new ProducerRecord<>("operations", (int) accountId, "" + (random.nextInt(1000) - 500))); 74 | 75 | if (i % 100 == 99) { 76 | try { 77 | Thread.sleep(2000L); 78 | } 79 | catch (InterruptedException e) { 80 | return; 81 | } 82 | } 83 | 84 | } 85 | } 86 | // end::produce[] 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/SampleDecryptingConsumer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.generatedkey; 21 | 22 | import java.util.Collections; 23 | import java.util.Properties; 24 | 25 | import org.apache.kafka.clients.consumer.ConsumerConfig; 26 | import org.apache.kafka.clients.consumer.ConsumerRecords; 27 | import org.apache.kafka.clients.consumer.KafkaConsumer; 28 | import org.apache.kafka.common.serialization.LongDeserializer; 29 | import org.apache.kafka.common.serialization.StringDeserializer; 30 | 31 | import io.quicksign.kafka.crypto.CryptoDeserializerFactory; 32 | import io.quicksign.kafka.crypto.Decryptor; 33 | import io.quicksign.kafka.crypto.encryption.DefaultDecryptor; 34 | import io.quicksign.kafka.crypto.generatedkey.MasterKeyEncryption; 35 | import io.quicksign.kafka.crypto.generatedkey.PerRecordKeyProvider; 36 | 37 | public class SampleDecryptingConsumer implements Runnable { 38 | 39 | private final MasterKeyEncryption masterKeyEncryption; 40 | 41 | public SampleDecryptingConsumer(MasterKeyEncryption masterKeyEncryption) { 42 | 43 | this.masterKeyEncryption = masterKeyEncryption; 44 | } 45 | 46 | 47 | @Override 48 | public void run() { 49 | 50 | // tag::consume[] 51 | // The key is embedded in each message 52 | PerRecordKeyProvider keyProvider = new PerRecordKeyProvider(masterKeyEncryption); 53 | 54 | // The payload is encrypted using AES 55 | AesGcmNoPaddingCryptoAlgorithm cryptoAlgorithm = new AesGcmNoPaddingCryptoAlgorithm(); 56 | Decryptor decryptor = new DefaultDecryptor(keyProvider, cryptoAlgorithm); 57 | 58 | // Construct decrypting deserializer 59 | CryptoDeserializerFactory cryptoDeserializerFactory = new CryptoDeserializerFactory(decryptor); 60 | 61 | Properties consumerProperties = new Properties(); 62 | consumerProperties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 63 | consumerProperties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "samplecrypted"); 64 | consumerProperties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); 65 | 66 | try (KafkaConsumer consumer = new KafkaConsumer( 67 | consumerProperties, 68 | new LongDeserializer(), 69 | cryptoDeserializerFactory.buildFrom(new StringDeserializer()))) { 70 | 71 | consumer.subscribe(Collections.singleton("sampletopic")); 72 | for (; true; ) { 73 | ConsumerRecords records = consumer.poll(1000L); 74 | records.forEach( 75 | record -> System.out.println( 76 | "-------------------------------------------------------------\n" + 77 | "decrypted record: key=" + record.key() + ", offset=" + record.offset() + ", value=" + record.value() + 78 | "\n-------------------------------------------------------------\n\n" 79 | ) 80 | ); 81 | } 82 | } 83 | // end::consume[] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /samples/keyrepo-sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 4.0.0 19 | 20 | io.quicksign 21 | kafka-encryption-keyrepo-sample 22 | 0.0.0-SNAPSHOT 23 | jar 24 | kafka-encryption-keyrepo-sample 25 | Kafka Record Encryption - key repository sample 26 | 27 | 28 | 1.8 29 | 1.8 30 | 1.8 31 | UTF-8 32 | 33 | 0.2.0 34 | 35 | 36 | 37 | 38 | 39 | io.quicksign 40 | kafka-encryption-core 41 | ${kafka-encryption.version} 42 | 43 | 44 | 45 | io.quicksign 46 | kafka-encryption-generatedkey 47 | ${kafka-encryption.version} 48 | 49 | 50 | 51 | io.quicksign 52 | kafka-encryption-keyrepository 53 | ${kafka-encryption.version} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.apache.maven.plugins 62 | maven-shade-plugin 63 | 3.2.0 64 | 65 | 66 | package 67 | 68 | shade 69 | 70 | 71 | true 72 | 73 | 74 | io.quicksign.kafka.crypto.samples.keyrepo.SamplesMain 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.apache.maven.plugins 83 | maven-deploy-plugin 84 | 2.7 85 | 86 | true 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /samples/generatedkey-sample/src/main/java/io/quicksign/kafka/crypto/samples/generatedkey/SampleProducer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.generatedkey; 21 | 22 | import java.util.Properties; 23 | 24 | import org.apache.kafka.clients.producer.KafkaProducer; 25 | import org.apache.kafka.clients.producer.ProducerConfig; 26 | import org.apache.kafka.clients.producer.ProducerRecord; 27 | import org.apache.kafka.common.serialization.LongSerializer; 28 | import org.apache.kafka.common.serialization.StringSerializer; 29 | 30 | import io.quicksign.kafka.crypto.Encryptor; 31 | import io.quicksign.kafka.crypto.encryption.DefaultEncryptor; 32 | import io.quicksign.kafka.crypto.generatedkey.AES256CryptoKeyGenerator; 33 | import io.quicksign.kafka.crypto.generatedkey.KeyPerRecordKeyReferenceExtractor; 34 | import io.quicksign.kafka.crypto.generatedkey.MasterKeyEncryption; 35 | import io.quicksign.kafka.crypto.generatedkey.PerRecordKeyProvider; 36 | import io.quicksign.kafka.crypto.pairing.serializer.CryptoSerializerPairFactory; 37 | import io.quicksign.kafka.crypto.pairing.serializer.SerializerPair; 38 | 39 | public class SampleProducer implements Runnable { 40 | 41 | private final MasterKeyEncryption masterKeyEncryption; 42 | 43 | public SampleProducer(MasterKeyEncryption masterKeyEncryption) { 44 | this.masterKeyEncryption = masterKeyEncryption; 45 | } 46 | 47 | @Override 48 | public void run() { 49 | 50 | // tag::produce[] 51 | // Use an AES256 key generator 52 | AES256CryptoKeyGenerator cryptoKeyGenerator = new AES256CryptoKeyGenerator(); 53 | 54 | // Generate a different key for each message and encrypt it using the master key 55 | KeyPerRecordKeyReferenceExtractor keyReferenceExtractor = new KeyPerRecordKeyReferenceExtractor( 56 | cryptoKeyGenerator, masterKeyEncryption); 57 | 58 | // The key is embedded in each message 59 | PerRecordKeyProvider keyProvider = new PerRecordKeyProvider(masterKeyEncryption); 60 | 61 | // The payload is encrypted using AES 62 | AesGcmNoPaddingCryptoAlgorithm cryptoAlgorithm = new AesGcmNoPaddingCryptoAlgorithm(); 63 | Encryptor encryptor = new DefaultEncryptor(keyProvider, cryptoAlgorithm); 64 | 65 | // Wrap base LongSerializer and StringSerializer with encrypted wrappers 66 | CryptoSerializerPairFactory cryptoSerializerPairFactory = new CryptoSerializerPairFactory(encryptor, 67 | keyReferenceExtractor); 68 | SerializerPair serializerPair = cryptoSerializerPairFactory.build(new LongSerializer(), new StringSerializer()); 69 | 70 | Properties producerProperties = new Properties(); 71 | producerProperties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 72 | 73 | try (KafkaProducer producer = 74 | new KafkaProducer<>(producerProperties, serializerPair.getKeySerializer(), serializerPair.getValueSerializer())) { 75 | 76 | for (long i = 0L; i < Long.MAX_VALUE; i++) { 77 | producer.send(new ProducerRecord<>("sampletopic", i, "test number " + i)); 78 | if (i % 10 == 9) { 79 | 80 | try { 81 | Thread.sleep(1000L); 82 | } 83 | catch (InterruptedException e) { 84 | return; 85 | } 86 | } 87 | } 88 | } 89 | // end::produce[] 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/pairing/serdes/CryptoSerdeFactory.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.pairing.serdes; 21 | 22 | import org.apache.kafka.common.serialization.Serde; 23 | import org.apache.kafka.common.serialization.Serdes; 24 | 25 | import io.quicksign.kafka.crypto.CryptoDeserializer; 26 | import io.quicksign.kafka.crypto.CryptoSerializer; 27 | import io.quicksign.kafka.crypto.Decryptor; 28 | import io.quicksign.kafka.crypto.Encryptor; 29 | import io.quicksign.kafka.crypto.pairing.internal.CryptoAwareSerializerWrapper; 30 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 31 | 32 | /** 33 | * Factory for pairing 2 serde using encryption. 34 | *

    35 | *
  • The serializer of the keySerde will be wrapped to call the {@link KeyReferenceExtractor}
  • 36 | *
  • The serializer of the valueSerde will be wrapped into a {@link CryptoSerializer}
  • 37 | *
  • The deserializer of the valueSerde will be wrapped into a {@link CryptoDeserializer}
  • 38 | *
39 | * The keyref extracted by the wrapped key serializer will be shared with the wrapped value serializer using a {@link ThreadLocal} 40 | * 41 | * @See io.quicksign.kafka.crypto.pairing.internal.CryptoAwareSerializerWrapper 42 | */ 43 | public class CryptoSerdeFactory implements SerdeFactory { 44 | 45 | private final Encryptor encryptor; 46 | private final Decryptor decryptor; 47 | private final KeyReferenceExtractor keyReferenceExtractor; 48 | 49 | /** 50 | * @param encryptor used for value encryption 51 | * @param decryptor used for value decryption 52 | * @param keyReferenceExtractor used to 53 | */ 54 | public CryptoSerdeFactory(Encryptor encryptor, Decryptor decryptor, KeyReferenceExtractor keyReferenceExtractor) { 55 | 56 | this.encryptor = encryptor; 57 | this.decryptor = decryptor; 58 | this.keyReferenceExtractor = keyReferenceExtractor; 59 | } 60 | 61 | public Serde buildFrom(Serde rawSerde) { 62 | return buildFrom(rawSerde, null); 63 | } 64 | 65 | private Serde buildFrom(Serde rawSerde, ThreadLocal keyRefHolder) { 66 | return Serdes.serdeFrom(new CryptoSerializer<>(rawSerde.serializer(), encryptor, keyRefHolder), 67 | new CryptoDeserializer<>(rawSerde.deserializer(), decryptor)); 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | @Override 74 | public SerdesPair buildSerdesPair(Serde keySerde, Serde valueSerde) { 75 | ThreadLocal keyRefHolder = new ThreadLocal<>(); 76 | Serde newKeySerde = Serdes.serdeFrom( 77 | new CryptoAwareSerializerWrapper(keySerde.serializer(), keyReferenceExtractor, keyRefHolder), 78 | keySerde.deserializer()); 79 | Serde newValueSerde = buildFrom(valueSerde, keyRefHolder); 80 | 81 | return new SerdesPair<>(newKeySerde, newValueSerde); 82 | } 83 | 84 | /** 85 | * used when the keyref can be deducted directly from the value 86 | * 87 | * @param valueSerde 88 | * @param 89 | * @return 90 | */ 91 | @Override 92 | public Serde buildSelfCryptoAwareSerde(Serde valueSerde) { 93 | ThreadLocal keyRefHolder = new ThreadLocal<>(); 94 | Serde cryptoSerde = buildFrom(valueSerde, keyRefHolder); 95 | Serde selfAwareSerde = Serdes.serdeFrom( 96 | new CryptoAwareSerializerWrapper(cryptoSerde.serializer(), keyReferenceExtractor, keyRefHolder), 97 | cryptoSerde.deserializer()); 98 | return selfAwareSerde; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /doc/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 19 | io.quicksign 20 | kafka-encryption-parent 21 | 0.0.0-SNAPSHOT 22 | 23 | 24 | 4.0.0 25 | 26 | kafka-encryption-doc 27 | 28 | 29 | 1.5.6 30 | 1.5.6 31 | 1.7.26 32 | 33 | 34 | 35 | ${basedir}/../target 36 | 37 | 38 | 39 | org.asciidoctor 40 | asciidoctor-maven-plugin 41 | 42 | ${basedir}/.. 43 | true 44 | 45 | ${project.groupId} 46 | ${project.artifactId} 47 | ${project.version} 48 | 49 | 50 | 51 | 52 | asciidoc-to-html 53 | generate-resources 54 | 55 | process-asciidoc 56 | 57 | 58 | html5 59 | coderay 60 | 61 | . 62 | left 63 | 4 64 | font 65 | true 66 | 67 | 68 | - 69 | true 70 | 71 | 72 | 73 | ${basedir}/.. 74 | ${project.build.directory}/generated-docs/ 75 | 76 | **/*.jpg 77 | **/*.gif 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.asciidoctor 90 | asciidoctor-maven-plugin 91 | ${asciidoctor.maven.plugin.version} 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /core/src/test/java/io/quicksign/kafka/crypto/CryptoDeserializerTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | import static org.mockito.BDDMockito.given; 24 | import static org.mockito.Mockito.verifyZeroInteractions; 25 | 26 | import java.nio.ByteBuffer; 27 | import java.nio.charset.StandardCharsets; 28 | 29 | import org.apache.kafka.common.header.Headers; 30 | import org.apache.kafka.common.header.internals.RecordHeaders; 31 | import org.apache.kafka.common.serialization.Deserializer; 32 | import org.junit.Test; 33 | import org.junit.runner.RunWith; 34 | import org.mockito.InjectMocks; 35 | import org.mockito.Mock; 36 | import org.mockito.runners.MockitoJUnitRunner; 37 | 38 | @RunWith(MockitoJUnitRunner.class) 39 | public class CryptoDeserializerTest { 40 | 41 | @Mock 42 | Decryptor decryptor; 43 | 44 | @Mock 45 | Deserializer rawDeserializer; 46 | 47 | @InjectMocks 48 | CryptoDeserializer cryptoDeserializer; 49 | 50 | @Test 51 | public void testDeserializeWhenKeyRefIsSet() { 52 | byte[] encoded = "encoded".getBytes(StandardCharsets.UTF_8); 53 | byte[] keyRef = "keyref1".getBytes(StandardCharsets.UTF_8); 54 | 55 | ByteBuffer encodedValue = ByteBuffer.allocate(KafkaCryptoConstants.ENCRYPTED_PREFIX.length + Integer.BYTES + keyRef.length + encoded.length); 56 | encodedValue.put(KafkaCryptoConstants.ENCRYPTED_PREFIX); 57 | encodedValue.putInt(keyRef.length); 58 | encodedValue.put(keyRef); 59 | encodedValue.put(encoded); 60 | 61 | 62 | given(decryptor.decrypt(encoded, keyRef)).willReturn("decoded".getBytes(StandardCharsets.UTF_8)); 63 | given(rawDeserializer.deserialize("topic1", new RecordHeaders(), "decoded".getBytes(StandardCharsets.UTF_8))).willReturn("deserialized value"); 64 | 65 | RecordHeaders recordHeaders = new RecordHeaders(); 66 | String value = cryptoDeserializer.deserialize("topic1", recordHeaders, encodedValue.array()); 67 | 68 | assertThat(value).isEqualTo("deserialized value"); 69 | 70 | assertThat(recordHeaders.lastHeader(KafkaCryptoConstants.KEY_REF_HEADER).value()).isEqualTo(keyRef); 71 | } 72 | 73 | @Test 74 | public void testDeserializeWhenKeyRefIsNotSet() { 75 | Headers headers = new RecordHeaders(); 76 | 77 | byte[] clearValue = "clearValue".getBytes(StandardCharsets.UTF_8); 78 | 79 | ByteBuffer rawValue = ByteBuffer.allocate(KafkaCryptoConstants.ENCRYPTED_PREFIX.length + Integer.BYTES + clearValue.length); 80 | rawValue.put(KafkaCryptoConstants.ENCRYPTED_PREFIX); 81 | rawValue.putInt(0); 82 | rawValue.put(clearValue); 83 | 84 | given(rawDeserializer.deserialize("topic1", headers, clearValue)).willReturn("deserialized value"); 85 | 86 | String value = cryptoDeserializer.deserialize("topic1", headers, rawValue.array()); 87 | 88 | assertThat(value).isEqualTo("deserialized value"); 89 | assertThat(headers.lastHeader(KafkaCryptoConstants.KEY_REF_HEADER).value()).isNull(); 90 | verifyZeroInteractions(decryptor); 91 | } 92 | 93 | @Test 94 | public void testDeserializeWhenNoEncryptionStructure() { 95 | Headers headers = new RecordHeaders(); 96 | 97 | byte[] clearValue = "clearValue".getBytes(StandardCharsets.UTF_8); 98 | 99 | given(rawDeserializer.deserialize("topic1", headers, clearValue)).willReturn("deserialized value"); 100 | 101 | String value = cryptoDeserializer.deserialize("topic1", headers, clearValue); 102 | 103 | assertThat(value).isEqualTo("deserialized value"); 104 | assertThat(headers.lastHeader(KafkaCryptoConstants.KEY_REF_HEADER).value()).isNull(); 105 | verifyZeroInteractions(decryptor); 106 | } 107 | 108 | 109 | } 110 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/CryptoSerializer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | import static io.quicksign.kafka.crypto.KafkaCryptoConstants.ENCRYPTED_PREFIX; 23 | import static io.quicksign.kafka.crypto.KafkaCryptoConstants.KEY_REF_HEADER; 24 | 25 | import java.nio.ByteBuffer; 26 | import java.util.Map; 27 | import java.util.Properties; 28 | 29 | import org.apache.kafka.common.header.Header; 30 | import org.apache.kafka.common.header.Headers; 31 | import org.apache.kafka.common.serialization.Serializer; 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | /** 36 | *

Serializer for encrypted

37 | * 38 | *

Data is first serialized with the underlying serializer

39 | * 40 | *

If a key reference is found either in the Kafka Header {@link KafkaCryptoConstants#KEY_REF_HEADER}, 41 | * or in the {@link ThreadLocal} keyRefHolder, then the serialized data is encrypted using the {@link Encryptor}. 42 | * If data has been encrypted, the result of the serialization is a byte array having the following structure: 43 | *

magic_bytes({@link KafkaCryptoConstants#ENCRYPTED_PREFIX})(6 bytes)|keyref.length(4 bytes)|keyref|encrypted_data
44 | *

45 | * 46 | *

If the result of encryption is null, then the result of serialization will be null.

47 | * 48 | *

If no key reference was found, the result of serialization will be directly the output of the underlying Serializer.

49 | * 50 | * 51 | *

Please note that this Serializer has no default constructor, so it can not be configured via properties. 52 | * So if you want to use it into a Producer, you will have to use {@link org.apache.kafka.clients.producer.KafkaProducer#KafkaProducer(Properties, Serializer, Serializer)} 53 | * or {@link org.apache.kafka.clients.producer.KafkaProducer#KafkaProducer(Map, Serializer, Serializer)} with your 54 | * instance of CryptoDeserializer 55 | *

56 | * 57 | * @param 58 | */ 59 | public class CryptoSerializer implements Serializer { 60 | 61 | private static final Logger log = LoggerFactory.getLogger(CryptoSerializer.class); 62 | 63 | 64 | private final Serializer rawSerializer; 65 | private final Encryptor encryptor; 66 | private final ThreadLocal keyRefHolder; 67 | 68 | /** 69 | * @param rawSerializer Serializer to serialize data before encryption 70 | * @param encryptor {@link Encryptor} to encrypt data 71 | * @param keyRefHolder {@link ThreadLocal} used to communicate the key reference when using Kafka Stream (unused for regular Kafka Producer) 72 | */ 73 | public CryptoSerializer(Serializer rawSerializer, Encryptor encryptor, ThreadLocal keyRefHolder) { 74 | this.rawSerializer = rawSerializer; 75 | this.encryptor = encryptor; 76 | this.keyRefHolder = keyRefHolder; 77 | } 78 | 79 | 80 | /** 81 | * serialize data with encryption (if needed). Key reference will be looked for exclusively in the Kafka Header 82 | * {@link KafkaCryptoConstants#KEY_REF_HEADER} 83 | * 84 | * @param topic 85 | * @param headers 86 | * @param data 87 | * @return 88 | */ 89 | @Override 90 | public byte[] serialize(String topic, Headers headers, T data) { 91 | byte[] serializedData = rawSerializer.serialize(topic, headers, data); 92 | if (serializedData == null) { 93 | return null; 94 | } 95 | Header keyReferenceHeader = headers.lastHeader(KEY_REF_HEADER); 96 | return encrypt(serializedData, keyReferenceHeader == null ? null : keyReferenceHeader.value()); 97 | } 98 | 99 | 100 | /** 101 | * serialize data with encryption (if needed). Key reference will be looked for into the {@link #keyRefHolder} 102 | * 103 | * @param topic 104 | * @param data 105 | * @return 106 | */ 107 | @Override 108 | public byte[] serialize(String topic, T data) { 109 | byte[] serializedData = rawSerializer.serialize(topic, data); 110 | if (serializedData == null) { 111 | return null; 112 | } 113 | return encrypt(serializedData, keyRefHolder == null ? null : keyRefHolder.get()); 114 | } 115 | 116 | private byte[] encrypt(byte[] serializedData, byte[] keyref) { 117 | if (keyref == null) { 118 | log.debug("keyref header not defined or null, we will send data unencrypted"); 119 | return serializedData; 120 | } 121 | byte[] encryptedData = encryptor.encrypt(serializedData, keyref); 122 | if (encryptedData == null) { 123 | return null; 124 | } 125 | return ByteBuffer.allocate(ENCRYPTED_PREFIX.length + Integer.BYTES + keyref.length + encryptedData.length) 126 | .put(ENCRYPTED_PREFIX) 127 | .putInt(keyref.length) 128 | .put(keyref) 129 | .put(encryptedData) 130 | .array(); 131 | } 132 | 133 | 134 | @Override 135 | public void configure(Map configs, boolean isKey) { 136 | 137 | rawSerializer.configure(configs, isKey); 138 | } 139 | 140 | @Override 141 | public void close() { 142 | rawSerializer.close(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /core/src/test/java/io/quicksign/kafka/crypto/CryptoSerializerTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | import static io.quicksign.kafka.crypto.KafkaCryptoConstants.ENCRYPTED_PREFIX; 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.mockito.BDDMockito.given; 25 | import static org.mockito.Mockito.verifyZeroInteractions; 26 | 27 | import java.nio.ByteBuffer; 28 | import java.nio.charset.StandardCharsets; 29 | 30 | import org.apache.kafka.common.header.Headers; 31 | import org.apache.kafka.common.header.internals.RecordHeaders; 32 | import org.apache.kafka.common.serialization.Serializer; 33 | import org.junit.Test; 34 | import org.junit.runner.RunWith; 35 | import org.mockito.InjectMocks; 36 | import org.mockito.Mock; 37 | import org.mockito.runners.MockitoJUnitRunner; 38 | 39 | @RunWith(MockitoJUnitRunner.class) 40 | public class CryptoSerializerTest { 41 | 42 | @Mock 43 | Serializer rawSerializer; 44 | 45 | @Mock 46 | Encryptor encryptor; 47 | 48 | @Mock 49 | ThreadLocal keyRefHolder; 50 | 51 | @InjectMocks 52 | CryptoSerializer cryptoSerializer; 53 | 54 | @Test 55 | public void testSerializeWhenKeyRefHeaderIsSet() { 56 | final String keyRef = "org1"; 57 | final int keyRefSize = toByteArray(keyRef).length; 58 | 59 | final String encoded = "encodedValue"; 60 | final int encodedSize = toByteArray(encoded).length; 61 | 62 | Headers headers = new RecordHeaders().add(KafkaCryptoConstants.KEY_REF_HEADER, toByteArray(keyRef)); 63 | given(rawSerializer.serialize("topic1", headers, "final value")) 64 | .willReturn("clear serialized value".getBytes(StandardCharsets.UTF_8)); 65 | given(encryptor.encrypt("clear serialized value".getBytes(StandardCharsets.UTF_8), toByteArray(keyRef))) 66 | .willReturn(toByteArray(encoded)); 67 | 68 | byte[] result = cryptoSerializer.serialize("topic1", headers, "final value"); 69 | 70 | assertThat(result).hasSize(ENCRYPTED_PREFIX.length + Integer.BYTES + keyRefSize + encodedSize); 71 | ByteBuffer byteBuffer = ByteBuffer.wrap(result); 72 | byte[] prefix = new byte[ENCRYPTED_PREFIX.length]; 73 | byteBuffer.get(prefix); 74 | assertThat(prefix).isEqualTo(ENCRYPTED_PREFIX); 75 | assertThat(byteBuffer.getInt()).isEqualTo(keyRefSize); 76 | byte[] resultKeyRef = new byte[keyRefSize]; 77 | byteBuffer.get(resultKeyRef); 78 | assertThat(resultKeyRef).isEqualTo(toByteArray(keyRef)); 79 | byte[] resultPayload = new byte[encodedSize]; 80 | byteBuffer.get(resultPayload); 81 | assertThat(resultPayload).isEqualTo(toByteArray(encoded)); 82 | 83 | } 84 | 85 | @Test 86 | public void testSerializeWhenKeyRefHeaderIsNotSet() { 87 | final String clearPayload = "clear serialized value"; 88 | final int clearPayloadSize = toByteArray(clearPayload).length; 89 | 90 | Headers headers = new RecordHeaders(); 91 | given(rawSerializer.serialize("topic1", headers, "final value")) 92 | .willReturn(toByteArray(clearPayload)); 93 | 94 | byte[] result = cryptoSerializer.serialize("topic1", headers, "final value"); 95 | 96 | assertThat(result).isEqualTo("clear serialized value".getBytes(StandardCharsets.UTF_8)); 97 | 98 | verifyZeroInteractions(encryptor); 99 | } 100 | 101 | @Test 102 | public void testSerializeWhenKeyRefHeaderIsSetToNull() { 103 | final String clearPayload = "clear serialized value"; 104 | final int clearPayloadSize = toByteArray(clearPayload).length; 105 | Headers headers = new RecordHeaders().add(KafkaCryptoConstants.KEY_REF_HEADER, null); 106 | given(rawSerializer.serialize("topic1", headers, "final value")) 107 | .willReturn(toByteArray(clearPayload)); 108 | 109 | byte[] result = cryptoSerializer.serialize("topic1", headers, "final value"); 110 | 111 | assertThat(result).isEqualTo("clear serialized value".getBytes(StandardCharsets.UTF_8)); 112 | verifyZeroInteractions(encryptor); 113 | } 114 | 115 | @Test 116 | public void testSerializeWithThreadLocal() { 117 | final String keyRef = "org1"; 118 | final int keyRefSize = toByteArray(keyRef).length; 119 | 120 | final String encoded = "encodedValue"; 121 | final int encodedSize = toByteArray(encoded).length; 122 | 123 | given(keyRefHolder.get()).willReturn(toByteArray(keyRef)); 124 | 125 | given(rawSerializer.serialize("topic1", "final value")) 126 | .willReturn("clear serialized value".getBytes(StandardCharsets.UTF_8)); 127 | given(encryptor.encrypt("clear serialized value".getBytes(StandardCharsets.UTF_8), toByteArray(keyRef))) 128 | .willReturn(toByteArray(encoded)); 129 | 130 | byte[] result = cryptoSerializer.serialize("topic1", "final value"); 131 | 132 | assertThat(result).hasSize(ENCRYPTED_PREFIX.length + Integer.BYTES + keyRefSize + encodedSize); 133 | ByteBuffer byteBuffer = ByteBuffer.wrap(result); 134 | byte[] prefix = new byte[ENCRYPTED_PREFIX.length]; 135 | byteBuffer.get(prefix); 136 | assertThat(prefix).isEqualTo(ENCRYPTED_PREFIX); 137 | assertThat(byteBuffer.getInt()).isEqualTo(keyRefSize); 138 | byte[] resultKeyRef = new byte[keyRefSize]; 139 | byteBuffer.get(resultKeyRef); 140 | assertThat(resultKeyRef).isEqualTo(toByteArray(keyRef)); 141 | byte[] resultPayload = new byte[encodedSize]; 142 | byteBuffer.get(resultPayload); 143 | assertThat(resultPayload).isEqualTo(toByteArray(encoded)); 144 | 145 | } 146 | 147 | private byte[] toByteArray(String s) { 148 | return s.getBytes(StandardCharsets.UTF_8); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /core/src/main/java/io/quicksign/kafka/crypto/CryptoDeserializer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto; 21 | 22 | import static io.quicksign.kafka.crypto.KafkaCryptoConstants.ENCRYPTED_PREFIX; 23 | import static io.quicksign.kafka.crypto.KafkaCryptoConstants.KEY_REF_HEADER; 24 | 25 | import java.nio.ByteBuffer; 26 | import java.util.Map; 27 | import java.util.Properties; 28 | 29 | import org.apache.kafka.common.header.Headers; 30 | import org.apache.kafka.common.header.internals.RecordHeaders; 31 | import org.apache.kafka.common.serialization.Deserializer; 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | import io.quicksign.kafka.crypto.utils.ArrayUtils; 36 | 37 | /** 38 | *

Deserializer for encrypted data

39 | * 40 | *

If the data to deserialize starts with the magic bytes {@link KafkaCryptoConstants#ENCRYPTED_PREFIX}, 41 | * then the data is decrypted using the {@link Decryptor}.The result is then deserialized using the underlying Deserializer. 42 | * If the data where not successfully decrypted, the result of the deserialization will be {@code null} 43 | *

44 | * 45 | *

If the data to deserialize does not starts with the magic bytes, it is directly deserialized using the underlying Deserializer 46 | *

47 | * 48 | *

Please note that this Deserializer has no default constructor, so it can not be configured via properties. 49 | * So if you want to use it into a Consumer, you will have to use {@link org.apache.kafka.clients.consumer.KafkaConsumer#KafkaConsumer(Properties, Deserializer, Deserializer)} 50 | * or {@link org.apache.kafka.clients.consumer.KafkaConsumer#KafkaConsumer(Map, Deserializer, Deserializer)} with your 51 | * instance of CryptoDeserializer 52 | *

53 | * 54 | * @param 55 | * @see CryptoSerializer 56 | * @see Decryptor 57 | */ 58 | public class CryptoDeserializer implements Deserializer { 59 | 60 | private static final Logger log = LoggerFactory.getLogger(CryptoDeserializer.class); 61 | 62 | private final Deserializer rawDeserializer; 63 | private final Decryptor decryptor; 64 | 65 | /** 66 | * @param rawDeserializer deserializer to deserialize clear data 67 | * @param decryptor Decryptor used to decrypt the data 68 | */ 69 | public CryptoDeserializer(Deserializer rawDeserializer, Decryptor decryptor) { 70 | 71 | this.rawDeserializer = rawDeserializer; 72 | this.decryptor = decryptor; 73 | } 74 | 75 | 76 | /** 77 | * deserialize the data (with decryption if needed) 78 | * The keyref used to deserialize the data will be added to the header {@link KafkaCryptoConstants#KEY_REF_HEADER} (may be {@code null}) 79 | * 80 | * @param topic 81 | * @param headers they will be enriched with header {@link KafkaCryptoConstants#KEY_REF_HEADER} 82 | * @param data 83 | * @return the deserialized data 84 | */ 85 | @Override 86 | public T deserialize(String topic, Headers headers, byte[] data) { 87 | if (data == null) { 88 | return null; 89 | } 90 | 91 | DecryptedDataWithKeyRef decryptedDataWithKeyRef = decrypt(data); 92 | 93 | 94 | T deserializedValue = rawDeserializer.deserialize(topic, headers, decryptedDataWithKeyRef.decryptedData); 95 | 96 | headers.add(KEY_REF_HEADER, decryptedDataWithKeyRef.keyRef); 97 | 98 | return deserializedValue; 99 | } 100 | 101 | @Override 102 | public void configure(Map configs, boolean isKey) { 103 | this.rawDeserializer.configure(configs, isKey); 104 | } 105 | 106 | /** 107 | * deserialize the data (with decryption if needed) 108 | * It is equivalent to: 109 | *
{@code
110 |      *  deserialize(topic, new RecordHeaders(), data);
111 |      * }
112 | * 113 | * @param topic 114 | * @param data 115 | * @return 116 | * @see #deserialize(String, Headers, byte[]) 117 | */ 118 | @Override 119 | public T deserialize(String topic, byte[] data) { 120 | return deserialize(topic, new RecordHeaders(), data); 121 | } 122 | 123 | 124 | private DecryptedDataWithKeyRef decrypt(byte[] data) { 125 | if (ArrayUtils.startWith(data, ENCRYPTED_PREFIX)) { 126 | ByteBuffer byteBuffer = ByteBuffer.wrap(data, 127 | ENCRYPTED_PREFIX.length, data.length - ENCRYPTED_PREFIX.length); 128 | int keyRefLength = byteBuffer.getInt(); 129 | byte[] decryptedData; 130 | byte[] keyRef = null; 131 | if (keyRefLength == 0) { 132 | log.debug("not key ref, data are not encrypted"); 133 | decryptedData = new byte[byteBuffer.remaining()]; 134 | byteBuffer.get(decryptedData); 135 | } 136 | else { 137 | keyRef = new byte[keyRefLength]; 138 | byteBuffer.get(keyRef); 139 | byte[] encryptedData = new byte[byteBuffer.remaining()]; 140 | byteBuffer.get(encryptedData); 141 | decryptedData = decryptor.decrypt(encryptedData, keyRef); 142 | 143 | } 144 | return new DecryptedDataWithKeyRef(keyRef, decryptedData); 145 | } 146 | else { 147 | return new DecryptedDataWithKeyRef(null, data); 148 | } 149 | 150 | } 151 | 152 | 153 | @Override 154 | public void close() { 155 | this.rawDeserializer.close(); 156 | } 157 | 158 | 159 | private static class DecryptedDataWithKeyRef { 160 | 161 | private final byte[] keyRef; 162 | private final byte[] decryptedData; 163 | 164 | DecryptedDataWithKeyRef(byte[] keyRef, byte[] decryptedData) { 165 | 166 | this.keyRef = keyRef; 167 | this.decryptedData = decryptedData; 168 | } 169 | 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /samples/kafkastream-with-keyrepo-sample/src/main/java/io/quicksign/kafka/crypto/samples/stream/keyrepo/SampleStream.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Kafka Encryption 4 | * %% 5 | * Copyright (C) 2018 Quicksign 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package io.quicksign.kafka.crypto.samples.stream.keyrepo; 21 | 22 | import java.util.Properties; 23 | 24 | import org.apache.kafka.common.serialization.Serdes; 25 | import org.apache.kafka.streams.KafkaStreams; 26 | import org.apache.kafka.streams.StreamsBuilder; 27 | import org.apache.kafka.streams.StreamsConfig; 28 | import org.apache.kafka.streams.kstream.Materialized; 29 | import org.apache.kafka.streams.state.KeyValueBytesStoreSupplier; 30 | import org.apache.kafka.streams.state.KeyValueIterator; 31 | import org.apache.kafka.streams.state.QueryableStoreTypes; 32 | import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; 33 | import org.apache.kafka.streams.state.Stores; 34 | 35 | import com.fasterxml.jackson.databind.ObjectMapper; 36 | import com.fasterxml.jackson.databind.node.ArrayNode; 37 | import com.fasterxml.jackson.databind.node.JsonNodeFactory; 38 | import com.fasterxml.jackson.databind.node.ObjectNode; 39 | import io.quicksign.kafka.crypto.Decryptor; 40 | import io.quicksign.kafka.crypto.Encryptor; 41 | import io.quicksign.kafka.crypto.encryption.DefaultDecryptor; 42 | import io.quicksign.kafka.crypto.encryption.DefaultEncryptor; 43 | import io.quicksign.kafka.crypto.encryption.KeyProvider; 44 | import io.quicksign.kafka.crypto.pairing.keyextractor.KeyReferenceExtractor; 45 | import io.quicksign.kafka.crypto.pairing.serdes.CryptoSerdeFactory; 46 | import io.quicksign.kafka.crypto.pairing.serdes.SerdesPair; 47 | 48 | public class SampleStream implements Runnable { 49 | 50 | private final String name; 51 | private final KeyProvider keyProvider; 52 | private final KeyReferenceExtractor keyReferenceExtractor; 53 | 54 | public SampleStream(String name, KeyProvider keyProvider, KeyReferenceExtractor keyReferenceExtractor) { 55 | 56 | this.name = name; 57 | this.keyProvider = keyProvider; 58 | this.keyReferenceExtractor = keyReferenceExtractor; 59 | } 60 | 61 | 62 | @Override 63 | public void run() { 64 | try { 65 | // tag::stream[] 66 | AesGcmNoPaddingCryptoAlgorithm cryptoAlgorithm = new AesGcmNoPaddingCryptoAlgorithm(); 67 | Decryptor decryptor = new DefaultDecryptor(keyProvider, cryptoAlgorithm); 68 | Encryptor encryptor = new DefaultEncryptor(keyProvider, cryptoAlgorithm); 69 | CryptoSerdeFactory cryptoSerdeFactory = new CryptoSerdeFactory(encryptor, decryptor, keyReferenceExtractor); 70 | 71 | KeyValueBytesStoreSupplier storeSupplier = Stores.inMemoryKeyValueStore(name + "_balance"); 72 | 73 | 74 | Properties props = new Properties(); 75 | props.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, name); 76 | props.setProperty(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 77 | props.setProperty(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName()); 78 | props.setProperty(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName()); 79 | 80 | 81 | StreamsBuilder streamsBuilder = new StreamsBuilder(); 82 | 83 | SerdesPair serdesPair = cryptoSerdeFactory.buildSerdesPair(Serdes.Integer(), Serdes.String()); 84 | 85 | streamsBuilder.stream("operations", serdesPair.toConsumed()) 86 | .filter((i, s) -> s != null) // messages that were not decrypted (because key not in repository) are null 87 | .groupByKey() 88 | .reduce((s1, s2) -> "" + (Integer.valueOf(s1) + Integer.valueOf(s2)), 89 | serdesPair.applyTo(Materialized.as(storeSupplier))); 90 | 91 | KafkaStreams kafkaStreams = new KafkaStreams(streamsBuilder.build(), props); 92 | kafkaStreams.start(); 93 | 94 | // end::stream[] 95 | 96 | ObjectMapper mapper = new ObjectMapper(); 97 | 98 | for (; true; ) { 99 | try { 100 | Thread.sleep(3000L); 101 | } 102 | catch (InterruptedException e) { 103 | return; 104 | } 105 | try { 106 | 107 | // tag::display[] 108 | ReadOnlyKeyValueStore store = kafkaStreams.store( 109 | name + "_balance", QueryableStoreTypes.keyValueStore() 110 | ); 111 | 112 | ArrayNode arrayNode = JsonNodeFactory.instance.arrayNode(); 113 | 114 | KeyValueIterator iterator = store.all(); 115 | iterator.forEachRemaining(kv -> { 116 | ObjectNode node = JsonNodeFactory.instance.objectNode(); 117 | node.put("account", kv.key); 118 | node.put("balance", kv.value); 119 | arrayNode.add(node); 120 | }); 121 | 122 | ObjectNode node = JsonNodeFactory.instance.objectNode(); 123 | node.set(name, arrayNode); 124 | 125 | System.out.println( 126 | "--[look up from KV store]------------------------------------\n" + 127 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(node) + 128 | "\n--------------------------------------\n\n" 129 | ); 130 | // end::display[] 131 | } 132 | catch (Exception e) { 133 | 134 | } 135 | } 136 | } 137 | catch (Throwable e) { 138 | System.out.println("error on " + name); 139 | e.printStackTrace(); 140 | } 141 | 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kafka-encryption 2 | 3 | [![Build Status](https://travis-ci.com/QuickSign/kafka-encryption.svg?branch=master)](https://travis-ci.com/QuickSign/kafka-encryption) 4 | 5 | Kafka-encryption is a Java framework that eases the encryption/decryption of Kafka 6 | record's value at the serializer/deserializer level. 7 | 8 | ## Design goals 9 | 10 | * Support or allow multiple encryption key management strategies (KMS, embedded, per message, rolling windows, per tenant, custom) 11 | * Support for Kafka Streams intermediate topics 12 | * Detect when a payload is not encrypted to skip decryption 13 | * Support for Spring Boot 14 | * Support for Camel 15 | 16 | ## Customization 17 | 18 | This framework exposes some high level Interfaces to let you customize the crypto 19 | Serializer/Deserializer internals. 20 | 21 | This framework is used on our platform. For obvious reason we do not reveal here our custom 22 | implementations of these interfaces. They would probably be useless to you anyway. 23 | 24 | However, and this is the good news, we provide in our examples some working implementations that 25 | you can definitely leverage. 26 | 27 | ## Terminology & basic explanation. 28 | 29 | As you explore the code or the examples, you may get confused by the terminology used. 30 | 31 | Do not confuse the Kafka `record's key` and the `encryption key` that is used to encrypt the record's value. 32 | 33 | You may also get confused by what we call a `key name` and a `key reference`. 34 | 35 | A `key name` is in general used to lookup an encryption key in a repository, but it could also be the `encryption key` itself. 36 | 37 | A `key reference` or `key ref` is derived from the `key name`. It can be for example an obfuscated or 38 | encrypted version of the `key name`. The `key ref` is stored in the record's value as a prefix of the encrypted value. . 39 | 40 | ## Examples 41 | 42 | We provide 3 examples that work out of the box. Do not use their code as is in production (we don't). 43 | Hopefully you can replace some of the implementations provided in the examples with your own. 44 | 45 | TIP: When studying the samples' code, to ease your pain start by studying 46 | the SamplesMain and SampleProducer. 47 | 48 | ### Example 1 - samples/generatedkey-sample : one encryption key per record 49 | 50 | This example uses the classic consumer API. It neither relies on the record's key nor on an 51 | encryption key repository. Instead the `encryption key` is encrypted and transmitted in the record's value. 52 | 53 | As a developer using the framework, in this example we provide 2 custom implementations to support our need. 54 | These implementations are used to construct the CryptoSerializerPairFactory. 55 | 56 | Here is roughly what this example demonstrates: 57 | 58 | __Serializer__ 59 | 60 | * Generates a new `encryption key` for each record 61 | * Encrypts the record's value using the `encryption key` (see AesGcmNoPaddingCryptoAlgorithm). 62 | * Uses the master encryption key (see KeyStoreBasedMasterKey) to encrypt the `encryption key`. The encrypted encryption key is the `key ref`. Note that the master encryption key is stored in a Java KeyStore which is itself protected by a password. 63 | 64 | __Deserializer__ 65 | 66 | * It extracts the `key ref` from the record's value. 67 | * Uses the master encryption key (see KeyStoreBasedMasterKey) to decrypt the `encryption key` out of the `key ref`. 68 | * Decrypts the record's value using the `encryption key` (see AesGcmNoPaddingCryptoAlgorithm). 69 | 70 | ### Example 2 - samples/kafkastreams-with-keyrepo-sample : one encryption key per record 71 | 72 | This example uses the Kafka Streams API. It creates a KTable, its content is also encrypted. 73 | We use one `encryption key` per record's key. 74 | The `encryption key` is stored in Java KeyStore, it is not transmitted in the record's value. 75 | 76 | As a developer using the framework, in this example we provide 4 custom implementations to support our need. 77 | These implementations are used to construct the CryptoSerializerPairFactory. 78 | 79 | Here is roughly what this example demonstrates: 80 | 81 | __Serializer__ 82 | 83 | * Uses the record's key as the `key name` (see SampleKeyNameExtractor) 84 | * Using the `key name`, looks up the `encryption key` from the KeyStoreBasedKeyRepository 85 | * Uses the SampleKeyNameObfuscator provided to create the `key ref` by simply swapping some bytes from the `key name`. 86 | * It encrypts the record's value using `encryption key` (see AesGcmNoPaddingCryptoAlgorithm) 87 | 88 | __Deserializer__ 89 | 90 | * Extracts the `key ref` from the record's value 91 | * Uses the SampleKeyNameObfuscator to obtain the `key name` out of the `key ref` 92 | * Looks up the `encryption key` from the KeyStoreBasedKeyRepository using the `key name` 93 | * Decrypts the record's value using the `encryption key` (see AesGcmNoPaddingCryptoAlgorithm) 94 | 95 | 96 | ### Example 3 - samples/keyrepo-sample : one encryption key per record's key. 97 | 98 | This example uses the classic consumer API. There is one `encryption key` per record's key. 99 | The `encryption key` is stored in an in memory encryption key repository, it is not transmitted in 100 | the record's value. 101 | 102 | As a developer using the framework, in this example we provide 4 custom implementations to support our need. 103 | These implementations are used to construct the CryptoSerializerPairFactory. 104 | 105 | Here is roughly what this example demonstrates: 106 | 107 | __Serializer__ 108 | 109 | * Uses the record's key as the `key name` (see SampleKeyNameExtractor) 110 | * Using the `key name`, looks up the `encryption key` from the SampleKeyRepository, a basic in memory encryption key repository. 111 | * Uses the SampleKeyNameObfuscator provided to create the `key ref` by simply swapping some bytes from the `key name`. 112 | * It encrypts the record's value using `encryption key` (see AesGcmNoPaddingCryptoAlgorithm) 113 | 114 | __Deserializer__ 115 | 116 | * Extracts the `key ref` from the record's value 117 | * Uses the SampleKeyNameObfuscator to obtain the `key name` out of the `key ref` 118 | * Looks up the `encryption key` from the SampleKeyRepository using the `key name` 119 | * Decrypts the record's value using the `encryption key` (see AesGcmNoPaddingCryptoAlgorithm) 120 | 121 | ### Troubleshooting 122 | 123 | In case the docker compose provided in the examples to run Kafka does not work for you, you may use this command: 124 | 125 | *On OSX and Windows* 126 | 127 | docker run --rm -p 2181:2181 -p 3030:3030 -p 8081-8083:8081-8083 -p 9581-9585:9581-9585 -p 9092:9092 -e ADV_HOST=192.168.99.100 landoop/fast-data-dev:2.0.1 128 | 129 | *On linux* 130 | 131 | docker run --rm --net=host -e ADV_HOST=localhost landoop/fast-data-dev:2.0.1 132 | 133 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 18 | 4.0.0 19 | 20 | io.quicksign 21 | kafka-encryption-parent 22 | 0.0.0-SNAPSHOT 23 | pom 24 | 25 | kafka-encryption-parent 26 | End to end encryption for Kafka 27 | 28 | https://github.com/Quicksign/kafka-encryption 29 | 30 | 31 | Julien Vaudour 32 | Quicksign 33 | julien.vaudour@quicksign.com 34 | 35 | 36 | 37 | 38 | 39 | The Apache Software License, Version 2.0 40 | http://www.apache.org/licenses/LICENSE-2.0.txt 41 | 42 | 43 | 44 | 45 | git@github.com:quicksign/kafka-encryption.git 46 | git@github.com:quicksign/kafka-encryption.git 47 | https://github.com/quicksign/kafka-encryption 48 | 49 | 50 | 51 | 1.8 52 | 1.8 53 | 1.8 54 | UTF-8 55 | 56 | 2.1.0 57 | 58 | 59 | 60 | 61 | ossrh 62 | https://oss.sonatype.org/content/repositories/snapshots 63 | 64 | 65 | ossrh 66 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | io.quicksign 75 | kafka-encryption-core 76 | ${project.version} 77 | 78 | 79 | 80 | io.quicksign 81 | kafka-encryption-generatedkey 82 | ${project.version} 83 | 84 | 85 | 86 | io.quicksign 87 | kafka-encryption-keyrepository 88 | ${project.version} 89 | 90 | 91 | 92 | org.slf4j 93 | slf4j-api 94 | 1.7.25 95 | 96 | 97 | 98 | org.apache.kafka 99 | kafka-clients 100 | ${kafka.version} 101 | 102 | 103 | 104 | org.apache.kafka 105 | kafka-streams 106 | ${kafka.version} 107 | 108 | 109 | 110 | 111 | 112 | core 113 | generatedkey 114 | keyrepository 115 | 116 | 117 | 118 | 119 | 120 | org.codehaus.mojo 121 | license-maven-plugin 122 | 1.16 123 | 124 | false 125 | true 126 | 127 | 128 | 129 | first 130 | 131 | update-file-header 132 | 133 | process-sources 134 | 135 | 2018 136 | Quicksign 137 | Kafka Encryption 138 | apache_v2 139 | false 140 | false 141 | 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-javadoc-plugin 148 | 3.0.1 149 | 150 | protected 151 | true 152 | -Xdoclint:none 153 | 154 | 155 | 156 | attach-javadocs 157 | 158 | jar 159 | 160 | 161 | 162 | 163 | 164 | org.apache.maven.plugins 165 | maven-source-plugin 166 | 3.0.1 167 | 168 | 169 | attach-sources 170 | 171 | jar-no-fork 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | publish 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-gpg-plugin 187 | 1.6 188 | 189 | 190 | sign-artifacts 191 | verify 192 | 193 | sign 194 | 195 | 196 | 197 | 198 | 199 | 200 | org.sonatype.plugins 201 | nexus-staging-maven-plugin 202 | 1.6.8 203 | true 204 | 205 | ossrh 206 | https://oss.sonatype.org/ 207 | true 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | --------------------------------------------------------------------------------