diff --git a/modules/crypto/build.gradle b/modules/crypto/build.gradle new file mode 100644 index 0000000000000..279e4b0b675c1 --- /dev/null +++ b/modules/crypto/build.gradle @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +apply plugin: 'opensearch.build' +apply plugin: 'opensearch.publish' + +forbiddenApis.ignoreFailures = false + +thirdPartyAudit.enabled = false +forbiddenApisTest.ignoreFailures = true +testingConventions.enabled = false + +opensearchplugin { + description 'Crypto module plugin for providing encryption and decryption support.' + classname 'org.opensearch.encryption.CryptoModulePlugin' +} + +dependencies { + // Common crypto classes + api project(':libs:opensearch-common') + + implementation "com.amazonaws:aws-encryption-sdk-java:1.7.0" + implementation "org.bouncycastle:bcprov-jdk15to18:${versions.bouncycastle}" + implementation "org.apache.commons:commons-lang3:${versions.commonslang}" + + //Tests + testImplementation "junit:junit:${versions.junit}" + testImplementation "org.hamcrest:hamcrest:${versions.hamcrest}" + testImplementation(project(":test:framework")) { + exclude group: 'org.opensearch', module: 'opensearch-encryption-sdk' + } + + compileOnly 'com.google.code.findbugs:annotations:3.0.1' +} + +tasks.named('forbiddenApisMain').configure { + // Only enable limited check because AD code has too many violations. + replaceSignatureFiles 'jdk-signatures' + signaturesFiles += files('src/forbidden/crypto-signatures.txt') +} + +// Encryption SDK files have missing java docs so disabling for the lib. +tasks.named('missingJavadoc').configure { + enabled = false +} + +forbiddenApisTest.setSignaturesFiles(files('src/forbidden/crypto-test-signatures.txt')) diff --git a/modules/crypto/licenses/aws-encryption-sdk-java-1.7.0.jar.sha1 b/modules/crypto/licenses/aws-encryption-sdk-java-1.7.0.jar.sha1 new file mode 100644 index 0000000000000..e0bb769bbf849 --- /dev/null +++ b/modules/crypto/licenses/aws-encryption-sdk-java-1.7.0.jar.sha1 @@ -0,0 +1 @@ +51704a672e65456d37f444c5992c079feff31218 \ No newline at end of file diff --git a/modules/crypto/licenses/aws-encryption-sdk-java-LICENSE.txt b/modules/crypto/licenses/aws-encryption-sdk-java-LICENSE.txt new file mode 100644 index 0000000000000..8dada3edaf50d --- /dev/null +++ b/modules/crypto/licenses/aws-encryption-sdk-java-LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/modules/crypto/licenses/aws-encryption-sdk-java-NOTICE.txt b/modules/crypto/licenses/aws-encryption-sdk-java-NOTICE.txt new file mode 100644 index 0000000000000..e32695955374a --- /dev/null +++ b/modules/crypto/licenses/aws-encryption-sdk-java-NOTICE.txt @@ -0,0 +1,11 @@ +AWS Encryption SDK +Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +THIRD PARTY COMPONENTS +********************** +This software includes third party software subject to the following copyrights: + +-Cryptographic functions from Bouncy Castle Crypto APIs for Java - Copyright +2000-2013 The Legion of the Bouncy Castle + +The licenses for these third party components are included in LICENSE.txt diff --git a/modules/crypto/licenses/bcprov-jdk15to18-1.75.jar.sha1 b/modules/crypto/licenses/bcprov-jdk15to18-1.75.jar.sha1 new file mode 100644 index 0000000000000..9911bb75f9209 --- /dev/null +++ b/modules/crypto/licenses/bcprov-jdk15to18-1.75.jar.sha1 @@ -0,0 +1 @@ +df22e1b6a9f6b218913f5b68dd16641344397fe0 \ No newline at end of file diff --git a/modules/crypto/licenses/bcprov-jdk15to18-LICENSE.txt b/modules/crypto/licenses/bcprov-jdk15to18-LICENSE.txt new file mode 100644 index 0000000000000..9f27bafe96885 --- /dev/null +++ b/modules/crypto/licenses/bcprov-jdk15to18-LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2000 - 2013 The Legion of the Bouncy Castle Inc. + (http://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/modules/crypto/licenses/bcprov-jdk15to18-NOTICE.txt b/modules/crypto/licenses/bcprov-jdk15to18-NOTICE.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/modules/crypto/licenses/commons-lang3-3.13.0.jar.sha1 b/modules/crypto/licenses/commons-lang3-3.13.0.jar.sha1 new file mode 100644 index 0000000000000..d0c2f2486ee1f --- /dev/null +++ b/modules/crypto/licenses/commons-lang3-3.13.0.jar.sha1 @@ -0,0 +1 @@ +b7263237aa89c1f99b327197c41d0669707a462e \ No newline at end of file diff --git a/modules/crypto/licenses/commons-lang3-LICENSE.txt b/modules/crypto/licenses/commons-lang3-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/modules/crypto/licenses/commons-lang3-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/modules/crypto/licenses/commons-lang3-NOTICE.txt b/modules/crypto/licenses/commons-lang3-NOTICE.txt new file mode 100644 index 0000000000000..13a3140897472 --- /dev/null +++ b/modules/crypto/licenses/commons-lang3-NOTICE.txt @@ -0,0 +1,5 @@ +Apache Commons Lang +Copyright 2001-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/modules/crypto/src/forbidden/crypto-signatures.txt b/modules/crypto/src/forbidden/crypto-signatures.txt new file mode 100644 index 0000000000000..3699186679924 --- /dev/null +++ b/modules/crypto/src/forbidden/crypto-signatures.txt @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +@defaultMessage use format with Locale +java.lang.String#format(java.lang.String,java.lang.Object[]) \ No newline at end of file diff --git a/modules/crypto/src/forbidden/crypto-test-signatures.txt b/modules/crypto/src/forbidden/crypto-test-signatures.txt new file mode 100644 index 0000000000000..3699186679924 --- /dev/null +++ b/modules/crypto/src/forbidden/crypto-test-signatures.txt @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +@defaultMessage use format with Locale +java.lang.String#format(java.lang.String,java.lang.Object[]) \ No newline at end of file diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/CryptoModulePlugin.java b/modules/crypto/src/main/java/org/opensearch/encryption/CryptoModulePlugin.java new file mode 100644 index 0000000000000..da77d5cfca8f2 --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/CryptoModulePlugin.java @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.encryption; + +import org.opensearch.common.crypto.CryptoHandler; +import org.opensearch.common.crypto.MasterKeyProvider; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.encryption.frame.AwsCrypto; +import org.opensearch.encryption.frame.EncryptionMetadata; +import org.opensearch.encryption.frame.FrameCryptoHandler; +import org.opensearch.encryption.keyprovider.CryptoMasterKey; +import org.opensearch.plugins.CryptoPlugin; +import org.opensearch.plugins.Plugin; + +import java.security.SecureRandom; +import java.util.concurrent.TimeUnit; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.ParsedCiphertext; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache; + +public class CryptoModulePlugin extends Plugin implements CryptoPlugin { + + private final int dataKeyCacheSize = 500; + private final String algorithm = "ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256"; + + // - Cache TTL and Jitter is used to decide the Crypto Cache TTL. + // - Random number between: (TTL Jitter, TTL - Jitter) + private final long dataKeyCacheTTL = TimeValue.timeValueDays(2).getMillis(); + private static final long dataKeyCacheJitter = TimeUnit.MINUTES.toMillis(30); // - 30 minutes + + public CryptoHandler getOrCreateCryptoHandler( + MasterKeyProvider keyProvider, + String keyProviderName, + String keyProviderType, + Runnable onClose + ) { + CachingCryptoMaterialsManager materialsManager = createMaterialsManager(keyProvider, keyProviderName, algorithm); + return createCryptoHandler(algorithm, materialsManager, keyProvider, onClose); + } + + // package private for tests + CryptoHandler createCryptoHandler( + String algorithm, + CachingCryptoMaterialsManager materialsManager, + MasterKeyProvider masterKeyProvider, + Runnable onClose + ) { + // Supporting only 256 bit algorithm as of now. To provide support for other bit size algorithms, necessary + // changes in key providers are required. Following 2 constraints should be satisfied to add support for + // another algorithm : + // 1. It should be safe to cache. Unsafe cache algorithms can't be used at it would require generation of data + // keys on every encrypt which is not a practical approach. + // 2. It shouldn't have any trailing metadata. This is needed to handle cases where full content is read + // till the length of the decrypted bytes are reached. This skips reading trailing metadata and closes + // remote streams. Remote store can throw an error for such reads saying that content wasn't fully read. + // With the above constraints ESDK, currently we can only add support for one algorithm. + if (algorithm.equals("ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256")) { + return new FrameCryptoHandler( + new AwsCrypto(materialsManager, CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256), + masterKeyProvider.getEncryptionContext(), + onClose + ); + } + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + + // Package private for tests + CachingCryptoMaterialsManager createMaterialsManager(MasterKeyProvider masterKeyProvider, String keyProviderName, String algorithm) { + SecureRandom r = new SecureRandom(); + long low = dataKeyCacheTTL - dataKeyCacheJitter; + long high = dataKeyCacheTTL + dataKeyCacheJitter; + long masterKeyCacheTTL = r.nextInt((int) (high - low)) + low; + + CryptoMasterKey cryptoMasterKey = new CryptoMasterKey(masterKeyProvider, keyProviderName, algorithm); + return CachingCryptoMaterialsManager.newBuilder() + .withMasterKeyProvider(cryptoMasterKey) + .withCache(new LocalCryptoMaterialsCache(dataKeyCacheSize)) + .withMaxAge(masterKeyCacheTTL, TimeUnit.MILLISECONDS) + .build(); + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/NoOpCryptoHandler.java b/modules/crypto/src/main/java/org/opensearch/encryption/NoOpCryptoHandler.java new file mode 100644 index 0000000000000..136ef50afbddc --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/NoOpCryptoHandler.java @@ -0,0 +1,132 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.encryption; + +import org.opensearch.common.crypto.CryptoHandler; +import org.opensearch.common.crypto.DecryptedRangedStreamProvider; +import org.opensearch.common.crypto.EncryptedHeaderContentSupplier; +import org.opensearch.common.io.InputStreamContainer; + +import java.io.IOException; +import java.io.InputStream; + +public class NoOpCryptoHandler implements CryptoHandler { + + /** + * No op - Initialises metadata store used in encryption. + * @return crypto metadata object constructed with encryption metadata like data key pair, encryption algorithm, etc. + */ + public Object initEncryptionMetadata() { + return new Object(); + } + + /** + * No op content size adjustment of length of a partial content used in partial encryption. + * + * @param cryptoContextObj stateful object for a request consisting of materials required in encryption. + * @param streamSize Size of the stream to be adjusted. + * @return Adjusted size of the stream. + */ + public long adjustContentSizeForPartialEncryption(Object cryptoContextObj, long streamSize) { + return streamSize; + } + + /** + * No op - Estimate length of the encrypted stream. + * + * @param cryptoMetadataObj crypto metadata instance + * @param contentLength Size of the raw content + * @return Calculated size of the encrypted stream for the provided raw stream. + */ + public long estimateEncryptedLengthOfEntireContent(Object cryptoMetadataObj, long contentLength) { + return contentLength; + } + + /** + * No op length estimation for a given content length. + * + * @param cryptoMetadataObj crypto metadata instance + * @param contentLength Size of the encrypted content + * @return Calculated size of the encrypted stream for the provided raw stream. + */ + public long estimateDecryptedLength(Object cryptoMetadataObj, long contentLength) { + return contentLength; + } + + /** + * No op encrypting stream wrapper. + * + * @param cryptoContextObj consists encryption metadata. + * @param stream Raw InputStream to encrypt + * @return encrypting stream wrapped around raw InputStream. + */ + public InputStreamContainer createEncryptingStream(Object cryptoContextObj, InputStreamContainer stream) { + return stream; + } + + /** + * No op encrypting stream provider for a part of content. + * + * @param cryptoContextObj stateful object for a request consisting of materials required in encryption. + * @param stream raw stream for which encrypted stream has to be created. + * @param totalStreams Number of streams being used for the entire content. + * @param streamIdx Index of the current stream. + * @return Encrypted stream for the provided raw stream. + */ + public InputStreamContainer createEncryptingStreamOfPart( + Object cryptoContextObj, + InputStreamContainer stream, + int totalStreams, + int streamIdx + ) { + return stream; + } + + /** + * + * @param encryptedHeaderContentSupplier Supplier used to fetch bytes from source for header creation + * @return parsed encryption metadata object + * @throws IOException if content fetch for header creation fails + */ + public Object loadEncryptionMetadata(EncryptedHeaderContentSupplier encryptedHeaderContentSupplier) throws IOException { + return new Object(); + } + + /** + * No op decrypting stream provider. + * + * @param encryptedStream to be decrypted. + * @return Decrypting wrapper stream + */ + public InputStream createDecryptingStream(InputStream encryptedStream) { + return encryptedStream; + } + + /** + * No Op decrypted stream range provider + * + * @param cryptoContext crypto metadata instance consisting of encryption metadata used in encryption. + * @param startPosOfRawContent starting position in the raw/decrypted content + * @param endPosOfRawContent ending position in the raw/decrypted content + * @return stream provider for decrypted stream for the specified range of content including adjusted range + */ + public DecryptedRangedStreamProvider createDecryptingStreamOfRange( + Object cryptoContext, + long startPosOfRawContent, + long endPosOfRawContent + ) { + long[] range = { startPosOfRawContent, endPosOfRawContent }; + return new DecryptedRangedStreamProvider(range, (encryptedStream) -> encryptedStream); + } + + @Override + public void close() { + // Nothing to close. + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/TrimmingStream.java b/modules/crypto/src/main/java/org/opensearch/encryption/TrimmingStream.java new file mode 100644 index 0000000000000..d6640bbe5e79e --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/TrimmingStream.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.encryption; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Trims content from a given source range to a target range. + */ +public class TrimmingStream extends InputStream { + + private final long sourceStart; + private final long sourceEnd; + private final long targetStart; + private final long targetEnd; + private final InputStream in; + + private long offsetFromStart = 0; + + public TrimmingStream(long sourceStart, long sourceEnd, long targetStart, long targetEnd, InputStream in) { + if (sourceStart < 0 + || targetStart < 0 + || targetEnd < 0 + || targetStart > targetEnd + || sourceStart > targetStart + || sourceEnd < targetEnd) { + throw new IllegalArgumentException("Invalid arguments to the bounded stream"); + } + + this.sourceStart = sourceStart; + this.sourceEnd = sourceEnd; + this.targetStart = targetStart; + this.targetEnd = targetEnd; + this.in = in; + } + + private void skipBytesOutsideBounds() throws IOException { + long relativeOffset = offsetFromStart + sourceStart; + + if (relativeOffset < targetStart) { + skipBytes(relativeOffset, targetStart); + } + + if (relativeOffset > targetEnd) { + skipBytes(relativeOffset, sourceEnd + 1); + } + } + + private void skipBytes(long offset, long end) throws IOException { + long bytesToSkip = end - offset; + while (bytesToSkip > 0) { + long skipped = skip(bytesToSkip); + if (skipped <= 0) { + // End of stream or unable to skip further + break; + } + bytesToSkip -= skipped; + } + } + + @Override + public int read() throws IOException { + skipBytesOutsideBounds(); + if (offsetFromStart + sourceStart > targetEnd) { + return -1; + } + int b = in.read(); + if (b != -1) { + offsetFromStart++; + } + // This call is made again to ensure that source stream is fully consumed when it reaches end of target range. + skipBytesOutsideBounds(); + return b; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + skipBytesOutsideBounds(); + if (offsetFromStart + sourceStart > targetEnd) { + return -1; + } + len = (int) Math.min(len, targetEnd - offsetFromStart - sourceStart + 1); + int bytesRead = in.read(b, off, len); + if (bytesRead != -1) { + offsetFromStart += bytesRead; + } + // This call is made again to ensure that source stream is fully consumed when it reaches end of target range. + skipBytesOutsideBounds(); + return bytesRead; + } + + /** + * Skips specified number of bytes of input. + * @param n the number of bytes to skip + * @return the actual number of bytes skipped + * @throws IOException if an I/O error has occurred + */ + public long skip(long n) throws IOException { + byte[] buf = new byte[512]; + long total = 0; + while (total < n) { + long len = n - total; + len = in.read(buf, 0, len < buf.length ? (int) len : buf.length); + if (len == -1) { + return total; + } + offsetFromStart += len; + total += len; + } + return total; + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/AwsCrypto.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/AwsCrypto.java new file mode 100644 index 0000000000000..241b82db5273c --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/AwsCrypto.java @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.encryption.frame; + +import org.opensearch.common.io.InputStreamContainer; + +import java.io.InputStream; +import java.util.Map; + +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.ParsedCiphertext; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.internal.LazyMessageCryptoHandler; +import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; + +public class AwsCrypto { + private final CryptoMaterialsManager materialsManager; + private final CryptoAlgorithm cryptoAlgorithm; + + public AwsCrypto(final CryptoMaterialsManager materialsManager, final CryptoAlgorithm cryptoAlgorithm) { + Utils.assertNonNull(materialsManager, "materialsManager"); + this.materialsManager = materialsManager; + this.cryptoAlgorithm = cryptoAlgorithm; + + } + + public EncryptionMetadata createCryptoContext(final Map encryptionContext, int frameSize) { + Utils.assertNonNull(encryptionContext, "encryptionContext"); + + EncryptionMaterialsRequest.Builder requestBuilder = EncryptionMaterialsRequest.newBuilder() + .setContext(encryptionContext) + .setRequestedAlgorithm(cryptoAlgorithm) + .setPlaintextSize(0) // To avoid skipping cache + .setCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt); + + return new EncryptionMetadata(frameSize, materialsManager.getMaterialsForEncrypt(requestBuilder.build())); + } + + public InputStreamContainer createEncryptingStream( + final InputStreamContainer stream, + int streamIdx, + int totalStreams, + int frameNumber, + EncryptionMetadata encryptionMetadata + ) { + + boolean isLastStream = streamIdx == totalStreams - 1; + boolean firstOperation = streamIdx == 0; + if (stream.getContentLength() % encryptionMetadata.getFrameSize() != 0 && !isLastStream) { + throw new AwsCryptoException( + "Length of each inputStream should be exactly divisible by frame size except " + + "the last inputStream. Current frame size is " + + encryptionMetadata.getFrameSize() + + " and inputStream length is " + + stream.getContentLength() + ); + } + final MessageCryptoHandler cryptoHandler = getEncryptingStreamHandler(frameNumber, firstOperation, encryptionMetadata); + CryptoInputStream cryptoInputStream = new CryptoInputStream<>(stream.getInputStream(), cryptoHandler, isLastStream); + cryptoInputStream.setMaxInputLength(stream.getContentLength()); + + long encryptedLength = 0; + if (streamIdx == 0) { + encryptedLength = encryptionMetadata.getCiphertextHeaderBytes().length; + } + if (streamIdx == (totalStreams - 1)) { + encryptedLength += estimateOutputSizeWithFooter( + encryptionMetadata.getFrameSize(), + encryptionMetadata.getNonceLen(), + encryptionMetadata.getCryptoAlgo().getTagLen(), + stream.getContentLength(), + encryptionMetadata.getCryptoAlgo() + ); + } else { + encryptedLength += estimatePartialOutputSize( + encryptionMetadata.getFrameSize(), + encryptionMetadata.getNonceLen(), + encryptionMetadata.getCryptoAlgo().getTagLen(), + stream.getContentLength() + ); + } + return new InputStreamContainer(cryptoInputStream, encryptedLength, -1); + } + + public MessageCryptoHandler getEncryptingStreamHandler( + int frameStartNumber, + boolean firstOperation, + EncryptionMetadata encryptionMetadata + ) { + return new LazyMessageCryptoHandler(info -> new EncryptionHandler(encryptionMetadata, firstOperation, frameStartNumber)); + } + + public long estimatePartialOutputSize(int frameLen, int nonceLen, int tagLen, long contentLength) { + return FrameEncryptionHandler.estimatePartialSizeFromMetadata(contentLength, false, frameLen, nonceLen, tagLen); + } + + public long estimateOutputSizeWithFooter(int frameLen, int nonceLen, int tagLen, long contentLength, CryptoAlgorithm cryptoAlgorithm) { + return FrameEncryptionHandler.estimatePartialSizeFromMetadata(contentLength, true, frameLen, nonceLen, tagLen) + + getTrailingSignatureSize(cryptoAlgorithm); + } + + public long estimateDecryptedSize(int frameLen, int nonceLen, int tagLen, long contentLength, CryptoAlgorithm cryptoAlgorithm) { + long contentLenWithoutTrailingSig = contentLength - getTrailingSignatureSize(cryptoAlgorithm); + return FrameDecryptionHandler.estimateDecryptedSize(contentLenWithoutTrailingSig, frameLen, nonceLen, tagLen); + } + + public int getTrailingSignatureSize(CryptoAlgorithm cryptoAlgorithm) { + return EncryptionHandler.getAlgoTrailingLength(cryptoAlgorithm); + } + + public CryptoInputStream createDecryptingStream(final InputStream inputStream) { + + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager); + return new CryptoInputStream<>(inputStream, cryptoHandler, true); + } + + public CryptoInputStream createDecryptingStream( + final InputStream inputStream, + final long size, + final ParsedCiphertext parsedCiphertext, + final int frameStartNum, + boolean isLastPart + ) { + + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager, parsedCiphertext, frameStartNum); + CryptoInputStream cryptoInputStream = new CryptoInputStream<>(inputStream, cryptoHandler, isLastPart); + cryptoInputStream.setMaxInputLength(size); + return cryptoInputStream; + } + +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/CipherHandler.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/CipherHandler.java new file mode 100644 index 0000000000000..941062dea015d --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/CipherHandler.java @@ -0,0 +1,109 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.opensearch.encryption.frame; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +import java.security.GeneralSecurityException; +import java.security.spec.AlgorithmParameterSpec; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; + +/** + * This class provides a cryptographic cipher handler powered by an underlying block cipher. The + * block cipher performs authenticated encryption of the provided bytes using Additional + * Authenticated Data (AAD). + * + *

This class implements a method called cipherData() that encrypts or decrypts a byte array by + * calling methods on the underlying block cipher. + */ +public class CipherHandler { + private final int cipherMode_; + private final SecretKey key_; + private final CryptoAlgorithm cryptoAlgorithm_; + private final Cipher cipher_; + + /** + * Process data through the cipher. + * + *

This method calls the update and doFinal methods on the underlying + * cipher to complete processing of the data. + * + * @param nonce the nonce to be used by the underlying cipher + * @param contentAad the optional additional authentication data to be used by the underlying + * cipher + * @param content the content to be processed by the underlying cipher + * @param off the offset into content array to be processed + * @param len the number of bytes to process + * @return the bytes processed by the underlying cipher + * @throws AwsCryptoException if cipher initialization fails + * @throws BadCiphertextException if processing the data through the cipher fails + */ + public byte[] cipherData(byte[] nonce, byte[] contentAad, final byte[] content, final int off, final int len) { + if (nonce.length != cryptoAlgorithm_.getNonceLen()) { + throw new IllegalArgumentException("Invalid nonce length"); + } + final AlgorithmParameterSpec spec = new GCMParameterSpec(cryptoAlgorithm_.getTagLen() * 8, nonce, 0, nonce.length); + + try { + cipher_.init(cipherMode_, key_, spec); + if (contentAad != null) { + cipher_.updateAAD(contentAad); + } + } catch (final GeneralSecurityException gsx) { + throw new AwsCryptoException(gsx); + } + try { + return cipher_.doFinal(content, off, len); + } catch (final GeneralSecurityException gsx) { + throw new BadCiphertextException(gsx); + } + } + + /** + * Create a cipher handler for processing bytes using an underlying block cipher. + * + * @param key the key to use in encrypting or decrypting bytes + * @param cipherMode the mode for processing the bytes as defined in {@link Cipher#init(int, + * java.security.Key)} + * @param cryptoAlgorithm the cryptography algorithm to be used by the underlying block cipher. + */ + public CipherHandler(final SecretKey key, final int cipherMode, final CryptoAlgorithm cryptoAlgorithm) { + this.cipherMode_ = cipherMode; + this.key_ = key; + this.cryptoAlgorithm_ = cryptoAlgorithm; + this.cipher_ = buildCipherObject(cryptoAlgorithm); + } + + private static Cipher buildCipherObject(final CryptoAlgorithm alg) { + try { + // Right now, just GCM is supported + return Cipher.getInstance("AES/GCM/NoPadding"); + } catch (final GeneralSecurityException ex) { + throw new IllegalStateException("Java does not support the requested algorithm", ex); + } + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/CryptoInputStream.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/CryptoInputStream.java new file mode 100644 index 0000000000000..e8d51fb2440d5 --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/CryptoInputStream.java @@ -0,0 +1,244 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.opensearch.encryption.frame; + +import java.io.IOException; +import java.io.InputStream; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; +import com.amazonaws.encryptionsdk.internal.Utils; + +import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; + +/** + * A CryptoInputStream is a subclass of java.io.InputStream. It performs cryptographic + * transformation of the bytes passing through it. + * + *

+ * The CryptoInputStream wraps a provided InputStream object and performs cryptographic + * transformation of the bytes read from the wrapped InputStream. It uses the cryptography handler + * provided during construction to invoke methods that perform the cryptographic transformations. + * + *

+ * In short, reading from the CryptoInputStream returns bytes that are the cryptographic + * transformations of the bytes read from the wrapped InputStream. + * + *

+ * For example, if the cryptography handler provides methods for decryption, the CryptoInputStream + * will read ciphertext bytes from the wrapped InputStream, decrypt, and return them as plaintext + * bytes. + * + *

+ * This class adheres strictly to the semantics, especially the failure semantics, of its ancestor + * class java.io.InputStream. This class overrides all the methods specified in its ancestor class. + * + *

+ * To instantiate an instance of this class, please see {@link AwsCrypto}. + * + * @param + * The type of {@link MasterKey}s used to manipulate the data. + */ +public class CryptoInputStream> extends InputStream { + private static final int MAX_READ_LEN = 4096; + + private byte[] outBytes_ = new byte[0]; + private int outStart_; + private int outEnd_; + private final InputStream inputStream_; + private final MessageCryptoHandler cryptoHandler_; + private boolean hasFinalCalled_; + private boolean hasProcessBytesCalled_; + private final boolean isLastPart_; + + /** + * Constructs a CryptoInputStream that wraps the provided InputStream object. It performs + * cryptographic transformation of the bytes read from the wrapped InputStream using the methods + * provided in the provided CryptoHandler implementation. + * + * @param inputStream + * the inputStream object to be wrapped. + * @param cryptoHandler + * the cryptoHandler implementation that provides the methods to use in performing + * cryptographic transformation of the bytes read from the inputStream. + */ + CryptoInputStream(final InputStream inputStream, final MessageCryptoHandler cryptoHandler, boolean isLastPart) { + inputStream_ = Utils.assertNonNull(inputStream, "inputStream"); + cryptoHandler_ = Utils.assertNonNull(cryptoHandler, "cryptoHandler"); + isLastPart_ = isLastPart; + } + + /** + * Fill the output bytes by reading from the wrapped InputStream and processing it through the + * crypto handler. + * + * @return the number of bytes processed and returned by the crypto handler. + */ + private int fillOutBytes() throws IOException, BadCiphertextException { + final byte[] inputStreamBytes = new byte[MAX_READ_LEN]; + + final int readLen = inputStream_.read(inputStreamBytes); + + outStart_ = 0; + + int processedLen = -1; + if (readLen < 0 && isLastPart_) { + // Mark end of stream until doFinal returns something. + + if (!hasFinalCalled_) { + int outOffset = 0; + int outLen = 0; + + // Handle the case where processBytes() was never called before. + // This happens with an empty file where the end of stream is + // reached on the first read attempt. In this case, + // processBytes() must be called so the header bytes are written + // during encryption. + if (!hasProcessBytesCalled_) { + outBytes_ = new byte[cryptoHandler_.estimateOutputSize(0)]; + outLen += cryptoHandler_.processBytes(inputStreamBytes, 0, 0, outBytes_, outOffset).getBytesWritten(); + outOffset += outLen; + } else { + outBytes_ = new byte[cryptoHandler_.estimateFinalOutputSize()]; + } + + // Get final bytes. + outLen += cryptoHandler_.doFinal(outBytes_, outOffset); + processedLen = outLen; + hasFinalCalled_ = true; + } + } else if (readLen > 0) { + // process the read bytes. + outBytes_ = new byte[cryptoHandler_.estimatePartialOutputSize(readLen)]; + processedLen = cryptoHandler_.processBytes(inputStreamBytes, 0, readLen, outBytes_, outStart_).getBytesWritten(); + hasProcessBytesCalled_ = true; + } + + outEnd_ = processedLen; + return processedLen; + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * This is thrown only during decryption if b contains invalid or corrupt + * ciphertext. + */ + @Override + public int read(final byte[] b, final int off, final int len) throws IllegalArgumentException, IOException, BadCiphertextException { + assertNonNull(b, "b"); + + if (len < 0 || off < 0) { + throw new IllegalArgumentException("Invalid values for offset: " + off + " and length: " + len); + } + + if (b.length == 0 || len == 0) { + return 0; + } + + // fill the output bytes if there aren't any left to return. + if ((outEnd_ - outStart_) <= 0) { + int newBytesLen = 0; + + // Block until a byte is read or end of stream in the underlying + // stream is reached. + while (newBytesLen == 0) { + newBytesLen = fillOutBytes(); + } + if (newBytesLen < 0) { + return -1; + } + } + + final int copyLen = Math.min((outEnd_ - outStart_), len); + System.arraycopy(outBytes_, outStart_, b, off, copyLen); + outStart_ += copyLen; + + return copyLen; + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * This is thrown only during decryption if b contains invalid or corrupt + * ciphertext. + */ + @Override + public int read(final byte[] b) throws IllegalArgumentException, IOException, BadCiphertextException { + return read(b, 0, b.length); + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * if b contains invalid or corrupt ciphertext. This is thrown only during + * decryption. + */ + @Override + public int read() throws IOException, BadCiphertextException { + final byte[] bArray = new byte[1]; + int result = 0; + + while (result == 0) { + result = read(bArray, 0, 1); + } + + if (result > 0) { + return (bArray[0] & 0xFF); + } else { + return result; + } + } + + @Override + public void close() throws IOException { + inputStream_.close(); + } + + /** + * Returns metadata associated with the performed cryptographic operation. + */ + @Override + public int available() throws IOException { + return (outBytes_.length + inputStream_.available()); + } + + /** + * Sets an upper bound on the size of the input data. This method should be called before reading any data from the + * stream. If this method is not called prior to reading any data, performance may be reduced (notably, it will not + * be possible to cache data keys when encrypting). + * Among other things, this size is used to enforce limits configured on the {@link CachingCryptoMaterialsManager}. + * If the input size set here is exceeded, an exception will be thrown, and the encyption or decryption will fail. + * + * @param size Maximum input size. + */ + public void setMaxInputLength(long size) { + cryptoHandler_.setMaxInputLength(size); + } + +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/DecryptionHandler.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/DecryptionHandler.java new file mode 100644 index 0000000000000..572e456788f97 --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/DecryptionHandler.java @@ -0,0 +1,532 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.opensearch.encryption.frame; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.DataKey; +import com.amazonaws.encryptionsdk.DefaultCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.MasterKeyProvider; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.CryptoHandler; +import com.amazonaws.encryptionsdk.internal.EncryptionHandler; +import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; +import com.amazonaws.encryptionsdk.internal.ProcessingSummary; +import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; +import com.amazonaws.encryptionsdk.model.CiphertextFooters; +import com.amazonaws.encryptionsdk.model.CiphertextHeaders; +import com.amazonaws.encryptionsdk.model.CiphertextType; +import com.amazonaws.encryptionsdk.model.ContentType; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; + +/** + * This class implements the CryptoHandler interface by providing methods for + * the decryption of ciphertext produced by the methods in + * {@link EncryptionHandler}. + * + *

+ * This class reads and parses the values in the ciphertext headers and + * delegates the decryption of the ciphertext to the + * content type parsed in the ciphertext headers. + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class DecryptionHandler> implements MessageCryptoHandler { + private final CryptoMaterialsManager materialsManager_; + + private final CiphertextHeaders ciphertextHeaders_; + private final CiphertextFooters ciphertextFooters_; + private boolean ciphertextHeadersParsed_; + + private CryptoHandler contentCryptoHandler_; + + private DataKey dataKey_; + private SecretKey decryptionKey_; + private CryptoAlgorithm cryptoAlgo_; + private Signature trailingSig_; + + private Map encryptionContext_ = null; + + private byte[] unparsedBytes_ = new byte[0]; + private boolean complete_ = false; + + private long ciphertextSizeBound_ = -1; + private long ciphertextBytesSupplied_ = 0; + + // These ctors are private to ensure type safety - we must ensure construction using a CMM results in a + // DecryptionHandler, not a DecryptionHandler, since the CryptoMaterialsManager is not itself + // genericized. + private DecryptionHandler(final CryptoMaterialsManager materialsManager) { + com.amazonaws.encryptionsdk.internal.Utils.assertNonNull(materialsManager, "materialsManager"); + + this.materialsManager_ = materialsManager; + ciphertextHeaders_ = new CiphertextHeaders(); + ciphertextFooters_ = new CiphertextFooters(); + } + + private DecryptionHandler(final CryptoMaterialsManager materialsManager, final CiphertextHeaders headers, final int frameStartNum) + throws AwsCryptoException { + com.amazonaws.encryptionsdk.internal.Utils.assertNonNull(materialsManager, "materialsManager"); + + materialsManager_ = materialsManager; + ciphertextHeaders_ = headers; + ciphertextFooters_ = new CiphertextFooters(); + readHeaderFields(headers, frameStartNum); + updateTrailingSignature(headers); + } + + /** + * Create a decryption handler using the provided master key. + * + *

+ * Note the methods in the provided master key are used in decrypting the + * encrypted data key parsed from the ciphertext headers. + * + * @param customerMasterKeyProvider + * the master key provider to use in picking a master key from + * the key blobs encoded in the provided ciphertext. + * @throws AwsCryptoException + * if the master key is null. + */ + @SuppressWarnings("unchecked") + public static > DecryptionHandler create(final MasterKeyProvider customerMasterKeyProvider) + throws AwsCryptoException { + Utils.assertNonNull(customerMasterKeyProvider, "customerMasterKeyProvider"); + + return (DecryptionHandler) create(new DefaultCryptoMaterialsManager(customerMasterKeyProvider)); + } + + /** + * Create a decryption handler using the provided materials manager. + * + *

+ * Note the methods in the provided materials manager are used in decrypting the encrypted data key + * parsed from the ciphertext headers. + * + * @param materialsManager + * the materials manager to use in decrypting the data key from the key blobs encoded + * in the provided ciphertext. + * @throws AwsCryptoException + * if the master key is null. + */ + public static DecryptionHandler create(final CryptoMaterialsManager materialsManager) throws AwsCryptoException { + return new DecryptionHandler(materialsManager); + } + + /** + * Create a decryption handler using the provided materials manager and already parsed {@code headers}. + * + *

+ * Note the methods in the provided materials manager are used in decrypting the encrypted data key + * parsed from the ciphertext headers. + * + * @param materialsManager the materials manager to use in decrypting the data key from the key + * blobs encoded in the provided ciphertext. + * @param headers already parsed headers which will not be passed into {@link + * #processBytes(byte[], int, int, byte[], int)} + * decryption; zero indicates no maximum + * @throws AwsCryptoException if the master key is null. + * @param frameStartNum Number from which assignment has to start for new frames + * @return instance of {@link DecryptionHandler} * + */ + public static DecryptionHandler create( + final CryptoMaterialsManager materialsManager, + final CiphertextHeaders headers, + final int frameStartNum + ) throws AwsCryptoException { + return new DecryptionHandler(materialsManager, headers, frameStartNum); + } + + /** + * Decrypt the ciphertext bytes provided in {@code in} and copy the plaintext bytes to + * {@code out}. + * + *

+ * This method consumes and parses the ciphertext headers. The decryption of the actual content + * is delegated to {@link FrameDecryptionHandler} based on the + * content type parsed in the ciphertext header. + * + * @param in + * the input byte array. + * @param off + * the offset into the in array where the data to be decrypted starts. + * @param len + * the number of bytes to be decrypted. + * @param out + * the output buffer the decrypted plaintext bytes go into. + * @param outOff + * the offset into the output byte array the decrypted data starts at. + * @return the number of bytes written to {@code out} and processed. + * + * @throws BadCiphertextException + * if the ciphertext header contains invalid entries or if the header integrity + * check fails. + * @throws AwsCryptoException + * if any of the offset or length arguments are negative or if the total bytes to + * decrypt exceeds the maximum allowed value. + */ + @Override + public ProcessingSummary processBytes(final byte[] in, final int off, final int len, final byte[] out, final int outOff) + throws BadCiphertextException, AwsCryptoException { + + if (len < 0 || off < 0) { + throw new AwsCryptoException("Invalid values for input offset: " + off + "and length:" + len); + } + + if (in.length == 0 || len == 0) { + return ProcessingSummary.ZERO; + } + + final long totalBytesToParse = unparsedBytes_.length + (long) len; + // check for integer overflow + if (totalBytesToParse > Integer.MAX_VALUE) { + throw new AwsCryptoException("Size of the total bytes to parse and decrypt exceeded allowed maximum:" + Integer.MAX_VALUE); + } + + checkSizeBound(len); + ciphertextBytesSupplied_ += len; + + final byte[] bytesToParse = new byte[(int) totalBytesToParse]; + final int leftoverBytes = unparsedBytes_.length; + // If there were previously unparsed bytes, add them as the first + // set of bytes to be parsed in this call. + System.arraycopy(unparsedBytes_, 0, bytesToParse, 0, unparsedBytes_.length); + System.arraycopy(in, off, bytesToParse, unparsedBytes_.length, len); + + int totalParsedBytes = 0; + if (ciphertextHeadersParsed_ == false) { + totalParsedBytes += ciphertextHeaders_.deserialize(bytesToParse, 0); + // When ciphertext headers are complete, we have the data + // key and cipher mode to initialize the underlying cipher + if (ciphertextHeaders_.isComplete() == true) { + readHeaderFields(ciphertextHeaders_, 1); + updateTrailingSignature(ciphertextHeaders_); + // reset unparsed bytes as parsing of ciphertext headers is + // complete. + unparsedBytes_ = new byte[0]; + } else { + // If there aren't enough bytes to parse ciphertext + // headers, we don't have anymore bytes to continue parsing. + // But first copy the leftover bytes to unparsed bytes. + unparsedBytes_ = Arrays.copyOfRange(bytesToParse, totalParsedBytes, bytesToParse.length); + return new ProcessingSummary(0, len); + } + } + + int actualOutLen = 0; + if (!contentCryptoHandler_.isComplete()) { + // if there are bytes to parse further, pass it off to underlying + // content cryptohandler. + if ((bytesToParse.length - totalParsedBytes) > 0) { + final ProcessingSummary contentResult = contentCryptoHandler_.processBytes( + bytesToParse, + totalParsedBytes, + bytesToParse.length - totalParsedBytes, + out, + outOff + ); + updateTrailingSignature(bytesToParse, totalParsedBytes, contentResult.getBytesProcessed()); + actualOutLen = contentResult.getBytesWritten(); + totalParsedBytes += contentResult.getBytesProcessed(); + + } + if (contentCryptoHandler_.isComplete()) { + actualOutLen += contentCryptoHandler_.doFinal(out, outOff + actualOutLen); + } + } + + if (contentCryptoHandler_.isComplete()) { + // If the crypto algorithm contains trailing signature, we will need to verify + // the footer of the message. + if (cryptoAlgo_.getTrailingSignatureLength() > 0) { + totalParsedBytes += ciphertextFooters_.deserialize(bytesToParse, totalParsedBytes); + if (ciphertextFooters_.isComplete() && trailingSig_ != null) { + try { + if (!trailingSig_.verify(ciphertextFooters_.getMAuth())) { + throw new BadCiphertextException("Bad trailing signature"); + } + } catch (final SignatureException ex) { + throw new BadCiphertextException("Bad trailing signature", ex); + } + complete_ = true; + } + } else { + complete_ = true; + } + } + return new ProcessingSummary(actualOutLen, totalParsedBytes - leftoverBytes); + } + + /** + * Finish processing of the bytes. + * + * @param out + * space for any resulting output data. + * @param outOff + * offset into {@code out} to start copying the data at. + * @return + * number of bytes written into {@code out}. + * @throws BadCiphertextException + * if the bytes do not decrypt correctly. + */ + @Override + public int doFinal(final byte[] out, final int outOff) throws BadCiphertextException { + // check if cryptohandler for content has been created. There are cases + // when it might not have been created such as when doFinal() is called + // before the ciphertext headers are fully received and parsed. + if (contentCryptoHandler_ == null) { + return 0; + } else { + + int result = contentCryptoHandler_.doFinal(out, outOff); + + if (!ciphertextHeaders_.isComplete() || !contentCryptoHandler_.isComplete()) { + throw new BadCiphertextException("Unable to process entire ciphertext."); + } + + return result; + } + } + + /** + * Return the size of the output buffer required for a + * processBytes plus a doFinal with an input of + * inLen bytes. + * + * @param inLen + * the length of the input. + * @return + * the space required to accommodate a call to processBytes and + * doFinal with input of size {@code inLen} bytes. + */ + @Override + public int estimateOutputSize(final int inLen) { + if (contentCryptoHandler_ != null) { + return contentCryptoHandler_.estimateOutputSize(inLen); + } else { + return Math.max(inLen, 0); + } + } + + @Override + public int estimatePartialOutputSize(int inLen) { + if (contentCryptoHandler_ != null) { + return contentCryptoHandler_.estimatePartialOutputSize(inLen); + } else { + return Math.max(inLen, 0); + } + } + + @Override + public int estimateFinalOutputSize() { + if (contentCryptoHandler_ != null) { + return contentCryptoHandler_.estimateFinalOutputSize(); + } else { + return 0; + } + } + + /** + * Return the encryption context. This value is parsed from the ciphertext. + * + * @return + * the key-value map containing the encryption client. + */ + @Override + public Map getEncryptionContext() { + return encryptionContext_; + } + + private void checkSizeBound(long additionalBytes) { + if (ciphertextSizeBound_ != -1 && ciphertextBytesSupplied_ + additionalBytes > ciphertextSizeBound_) { + throw new IllegalStateException("Ciphertext size exceeds size bound"); + } + } + + @Override + public void setMaxInputLength(long size) { + if (size < 0) { + throw Utils.cannotBeNegative("Max input length"); + } + + if (ciphertextSizeBound_ != -1 && ciphertextSizeBound_ < size) { + ciphertextSizeBound_ = size; + } + + // check that we haven't already exceeded the limit + checkSizeBound(0); + } + + /** + * Check integrity of the header bytes by processing the parsed MAC tag in + * the headers through the cipher. + * + * @param ciphertextHeaders + * the ciphertext headers object whose integrity needs to be + * checked. + */ + private void verifyHeaderIntegrity(final CiphertextHeaders ciphertextHeaders) throws BadCiphertextException { + final CipherHandler cipherHandler = new CipherHandler(decryptionKey_, Cipher.DECRYPT_MODE, cryptoAlgo_); + + try { + final byte[] headerTag = ciphertextHeaders.getHeaderTag(); + cipherHandler.cipherData( + ciphertextHeaders.getHeaderNonce(), + ciphertextHeaders.serializeAuthenticatedFields(), + headerTag, + 0, + headerTag.length + ); + } catch (BadCiphertextException e) { + throw new BadCiphertextException("Header integrity check failed.", e); + } + } + + /** + * Read the fields in the ciphertext headers to populate the corresponding + * instance variables used during decryption. + * + * @param ciphertextHeaders + * the ciphertext headers object to read. + */ + @SuppressWarnings("unchecked") + private void readHeaderFields(final CiphertextHeaders ciphertextHeaders, final int frameStartNum) { + cryptoAlgo_ = ciphertextHeaders.getCryptoAlgoId(); + + final CiphertextType ciphertextType = ciphertextHeaders.getType(); + if (ciphertextType != CiphertextType.CUSTOMER_AUTHENTICATED_ENCRYPTED_DATA) { + throw new BadCiphertextException("Invalid type in ciphertext."); + } + + final byte[] messageId = ciphertextHeaders.getMessageId(); + + encryptionContext_ = ciphertextHeaders.getEncryptionContextMap(); + + DecryptionMaterialsRequest request = DecryptionMaterialsRequest.newBuilder() + .setAlgorithm(cryptoAlgo_) + .setEncryptionContext(encryptionContext_) + .setEncryptedDataKeys(ciphertextHeaders.getEncryptedKeyBlobs()) + .build(); + + DecryptionMaterials result = materialsManager_.decryptMaterials(request); + + // noinspection unchecked + dataKey_ = (DataKey) result.getDataKey(); + PublicKey trailingPublicKey = result.getTrailingSignatureKey(); + + try { + decryptionKey_ = cryptoAlgo_.getEncryptionKeyFromDataKey(dataKey_.getKey(), ciphertextHeaders); + } catch (final InvalidKeyException ex) { + throw new AwsCryptoException(ex); + } + + if (cryptoAlgo_.getTrailingSignatureLength() > 0) { + Utils.assertNonNull(trailingPublicKey, "trailing public key"); + + TrailingSignatureAlgorithm trailingSignatureAlgorithm = TrailingSignatureAlgorithm.forCryptoAlgorithm(cryptoAlgo_); + + try { + trailingSig_ = Signature.getInstance(trailingSignatureAlgorithm.getHashAndSignAlgorithm()); + + trailingSig_.initVerify(trailingPublicKey); + } catch (GeneralSecurityException e) { + throw new AwsCryptoException(e); + } + } else { + if (trailingPublicKey != null) { + throw new AwsCryptoException("Unexpected trailing signature key in context"); + } + + trailingSig_ = null; + } + + final ContentType contentType = ciphertextHeaders.getContentType(); + + final short nonceLen = ciphertextHeaders.getNonceLength(); + final int frameLen = ciphertextHeaders.getFrameLength(); + + verifyHeaderIntegrity(ciphertextHeaders); + + // should never get here because an invalid content type is + // detected when parsing. + if (Objects.requireNonNull(contentType) == ContentType.FRAME) { + contentCryptoHandler_ = new FrameDecryptionHandler( + decryptionKey_, + (byte) nonceLen, + cryptoAlgo_, + messageId, + frameLen, + frameStartNum + ); + } + + ciphertextHeadersParsed_ = true; + } + + private void updateTrailingSignature(final CiphertextHeaders headers) { + if (trailingSig_ != null) { + final byte[] reserializedHeaders = headers.toByteArray(); + updateTrailingSignature(reserializedHeaders, 0, reserializedHeaders.length); + } + } + + private void updateTrailingSignature(byte[] input, int offset, int len) { + if (trailingSig_ != null) { + try { + trailingSig_.update(input, offset, len); + } catch (final SignatureException ex) { + throw new AwsCryptoException(ex); + } + } + } + + @Override + public CiphertextHeaders getHeaders() { + return ciphertextHeaders_; + } + + @Override + public List getMasterKeys() { + return Collections.singletonList(dataKey_.getMasterKey()); + } + + @Override + public boolean isComplete() { + return complete_; + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/EncryptionHandler.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/EncryptionHandler.java new file mode 100644 index 0000000000000..af4521cf4ee13 --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/EncryptionHandler.java @@ -0,0 +1,368 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.opensearch.encryption.frame; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.ECPrivateKey; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.CryptoHandler; +import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; +import com.amazonaws.encryptionsdk.internal.ProcessingSummary; +import com.amazonaws.encryptionsdk.model.CiphertextFooters; +import com.amazonaws.encryptionsdk.model.CiphertextHeaders; +import com.amazonaws.encryptionsdk.model.CiphertextType; +import com.amazonaws.encryptionsdk.model.ContentType; +import com.amazonaws.encryptionsdk.model.KeyBlob; + +/** + * This class implements the CryptoHandler interface by providing methods for the encryption of + * plaintext data. + * + *

+ * This class creates the ciphertext headers and delegates the encryption of the plaintext to the + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class EncryptionHandler implements MessageCryptoHandler { + + private final Map encryptionContext_; + private final CryptoAlgorithm cryptoAlgo_; + private final List masterKeys_; + private final List keyBlobs_; + private final byte version_; + private final CiphertextType type_; + private final byte nonceLen_; + private final PrivateKey trailingSignaturePrivateKey_; + private final MessageDigest trailingDigest_; + private final Signature trailingSig_; + + private final CiphertextHeaders ciphertextHeaders_; + private final byte[] ciphertextHeaderBytes_; + private final CryptoHandler contentCryptoHandler_; + + private boolean firstOperation_; + private boolean complete_ = false; + + private long plaintextBytes_ = 0; + private long plaintextByteLimit_ = -1; + + /** + * Create an encryption handler using the provided master key and encryption context. + * @param encryptionMetadata Context object created before encryption + * @param isFirstStream In case of first stream, file header is additionally created which consists of crypto + * materials. + * @param frameStartNumber Number from which assignment has to start for new frames + */ + public EncryptionHandler(EncryptionMetadata encryptionMetadata, boolean isFirstStream, int frameStartNumber) throws AwsCryptoException { + this.encryptionContext_ = encryptionMetadata.getEncryptionContext(); + this.cryptoAlgo_ = encryptionMetadata.getCryptoAlgo(); + this.masterKeys_ = encryptionMetadata.getMasterKeys(); + this.keyBlobs_ = encryptionMetadata.getKeyBlobs(); + this.trailingSignaturePrivateKey_ = encryptionMetadata.getTrailingSignaturePrivateKey(); + + if (keyBlobs_.isEmpty()) { + throw new IllegalArgumentException("No encrypted data keys in materials result"); + } + + if (trailingSignaturePrivateKey_ != null) { + trailingDigest_ = encryptionMetadata.getTrailingDigest(); + trailingSig_ = encryptionMetadata.getTrailingSig(); + } else { + trailingDigest_ = null; + trailingSig_ = null; + } + + version_ = encryptionMetadata.getVersion(); + type_ = encryptionMetadata.getType(); + nonceLen_ = encryptionMetadata.getNonceLen(); + ciphertextHeaders_ = encryptionMetadata.getCiphertextHeaders(); + ciphertextHeaderBytes_ = encryptionMetadata.getCiphertextHeaderBytes(); + firstOperation_ = isFirstStream; + + byte[] messageId_ = encryptionMetadata.getMessageId(); + + if (encryptionMetadata.getContentType() == ContentType.FRAME) { + contentCryptoHandler_ = new FrameEncryptionHandler( + encryptionMetadata.getEncryptionKey(), + nonceLen_, + cryptoAlgo_, + messageId_, + encryptionMetadata.getFrameSize(), + frameStartNumber + ); + } else {// should never get here because a valid content type is always + // set above based on the frame size. + throw new AwsCryptoException("Unknown content type."); + } + } + + /** + * Encrypt a block of bytes from {@code in} putting the plaintext result into {@code out}. + * + *

+ * It encrypts by performing the following operations: + *

    + *
  1. if this is the first call to encrypt, write the ciphertext headers to the output being + * returned.
  2. + *
  3. else, pass off the input data to underlying content cryptohandler.
  4. + *
+ * + * @param in + * the input byte array. + * @param off + * the offset into the in array where the data to be encrypted starts. + * @param len + * the number of bytes to be encrypted. + * @param out + * the output buffer the encrypted bytes go into. + * @param outOff + * the offset into the output byte array the encrypted data starts at. + * @return the number of bytes written to out and processed + * @throws AwsCryptoException + * if len or offset values are negative. + * @throws BadCiphertextException + * thrown by the underlying cipher handler. + */ + @Override + public ProcessingSummary processBytes(final byte[] in, final int off, final int len, final byte[] out, final int outOff) + throws AwsCryptoException, BadCiphertextException { + if (len < 0 || off < 0) { + throw new AwsCryptoException( + String.format(Locale.getDefault(), "Invalid values for input offset: %d and length: %d", off, len) + ); + } + + checkPlaintextSizeLimit(len); + + int actualOutLen = 0; + + if (firstOperation_ == true) { + System.arraycopy(ciphertextHeaderBytes_, 0, out, outOff, ciphertextHeaderBytes_.length); + actualOutLen += ciphertextHeaderBytes_.length; + + firstOperation_ = false; + } + + ProcessingSummary contentOut = contentCryptoHandler_.processBytes(in, off, len, out, outOff + actualOutLen); + actualOutLen += contentOut.getBytesWritten(); + updateTrailingSignature(out, outOff, actualOutLen); + plaintextBytes_ += contentOut.getBytesProcessed(); + return new ProcessingSummary(actualOutLen, contentOut.getBytesProcessed()); + } + + /** + * Finish encryption of the plaintext bytes. + * + * @param out + * space for any resulting output data. + * @param outOff + * offset into out to start copying the data at. + * @return number of bytes written into out. + * @throws BadCiphertextException + * thrown by the underlying cipher handler. + */ + @Override + public int doFinal(final byte[] out, final int outOff) throws BadCiphertextException { + if (complete_) { + throw new IllegalStateException("Attempted to call doFinal twice"); + } + + complete_ = true; + + checkPlaintextSizeLimit(0); + + int written = contentCryptoHandler_.doFinal(out, outOff); + updateTrailingSignature(out, outOff, written); + if (cryptoAlgo_.getTrailingSignatureLength() > 0) { + try { + CiphertextFooters footer = new CiphertextFooters(signContent()); + byte[] fBytes = footer.toByteArray(); + System.arraycopy(fBytes, 0, out, outOff + written, fBytes.length); + return written + fBytes.length; + } catch (final SignatureException ex) { + throw new AwsCryptoException(ex); + } + } else { + return written; + } + } + + private byte[] signContent() throws SignatureException { + if (trailingDigest_ != null) { + if (!trailingSig_.getAlgorithm().contains("ECDSA")) { + throw new UnsupportedOperationException("Signatures calculated in pieces is only supported for ECDSA."); + } + final byte[] digest = trailingDigest_.digest(); + return generateEcdsaFixedLengthSignature(digest); + } + return trailingSig_.sign(); + } + + private byte[] generateEcdsaFixedLengthSignature(final byte[] digest) throws SignatureException { + byte[] signature; + // Unfortunately, we need deterministic lengths some signatures are non-deterministic in length. + // So, retry until we get the right length :-( + do { + trailingSig_.update(digest); + signature = trailingSig_.sign(); + if (signature.length != cryptoAlgo_.getTrailingSignatureLength()) { + // Most of the time, a signature of the wrong length can be fixed + // be negating s in the signature relative to the group order. + ASN1Sequence seq = ASN1Sequence.getInstance(signature); + ASN1Integer r = (ASN1Integer) seq.getObjectAt(0); + ASN1Integer s = (ASN1Integer) seq.getObjectAt(1); + ECPrivateKey ecKey = (ECPrivateKey) trailingSignaturePrivateKey_; + s = new ASN1Integer(ecKey.getParams().getOrder().subtract(s.getPositiveValue())); + seq = new DERSequence(new ASN1Encodable[] { r, s }); + try { + signature = seq.getEncoded(); + } catch (IOException ex) { + throw new SignatureException(ex); + } + } + } while (signature.length != cryptoAlgo_.getTrailingSignatureLength()); + return signature; + } + + /** + * Return the size of the output buffer required for a {@code processBytes} plus a + * {@code doFinal} with an input of inLen bytes. + * + * @param inLen + * the length of the input. + * @return the space required to accommodate a call to processBytes and doFinal with len bytes + * of input. + */ + @Override + public int estimateOutputSize(final int inLen) { + int outSize = 0; + if (firstOperation_ == true) { + outSize += ciphertextHeaderBytes_.length; + } + outSize += contentCryptoHandler_.estimateOutputSize(inLen); + + outSize += getAlgoTrailingLength(cryptoAlgo_); + + return outSize; + } + + public static int getAlgoTrailingLength(CryptoAlgorithm cryptoAlgo) { + int outSize = 0; + if (cryptoAlgo.getTrailingSignatureLength() > 0) { + outSize += 2; // Length field in footer + outSize += cryptoAlgo.getTrailingSignatureLength(); + } + + return outSize; + } + + @Override + public int estimatePartialOutputSize(int inLen) { + int outSize = 0; + if (firstOperation_ == true) { + outSize += ciphertextHeaderBytes_.length; + } + outSize += contentCryptoHandler_.estimatePartialOutputSize(inLen); + + return outSize; + } + + @Override + public int estimateFinalOutputSize() { + return estimateOutputSize(0); + } + + /** + * Return the encryption context. + * + * @return the key-value map containing encryption context. + */ + @Override + public Map getEncryptionContext() { + return encryptionContext_; + } + + @Override + public CiphertextHeaders getHeaders() { + return ciphertextHeaders_; + } + + @Override + public void setMaxInputLength(long size) { + if (size < 0) { + throw Utils.cannotBeNegative("Max input length"); + } + + if (plaintextByteLimit_ == -1 || plaintextByteLimit_ > size) { + plaintextByteLimit_ = size; + } + + // check that we haven't already exceeded the limit + checkPlaintextSizeLimit(0); + } + + private void checkPlaintextSizeLimit(long additionalBytes) { + if (plaintextByteLimit_ != -1 && plaintextBytes_ + additionalBytes > plaintextByteLimit_) { + throw new IllegalStateException("Plaintext size exceeds max input size limit"); + } + } + + @Override + @SuppressWarnings("unchecked") + public List> getMasterKeys() { + // noinspection unchecked + return (List) masterKeys_; // This is unmodifiable + } + + private void updateTrailingSignature(byte[] input, int offset, int len) { + if (trailingDigest_ != null) { + trailingDigest_.update(input, offset, len); + } else if (trailingSig_ != null) { + try { + trailingSig_.update(input, offset, len); + } catch (final SignatureException ex) { + throw new AwsCryptoException(ex); + } + } + + } + + @Override + public boolean isComplete() { + return complete_; + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/EncryptionMetadata.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/EncryptionMetadata.java new file mode 100644 index 0000000000000..3663b25c2a8ae --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/EncryptionMetadata.java @@ -0,0 +1,252 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.opensearch.encryption.frame; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.Signature; +import java.util.List; +import java.util.Map; + +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.internal.EncryptionContextSerializer; +import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; +import com.amazonaws.encryptionsdk.model.CiphertextHeaders; +import com.amazonaws.encryptionsdk.model.CiphertextType; +import com.amazonaws.encryptionsdk.model.ContentType; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.KeyBlob; + +@SuppressWarnings({ "rawtypes" }) +public class EncryptionMetadata { + private static final CiphertextType CIPHERTEXT_TYPE = CiphertextType.CUSTOMER_AUTHENTICATED_ENCRYPTED_DATA; + + private final Map encryptionContext_; + private final CryptoAlgorithm cryptoAlgo; + private final List masterKeys; + private final List keyBlobs; + private final SecretKey encryptionKey; + private final byte version; + private final CiphertextType type; + private final byte nonceLen; + + private final CiphertextHeaders ciphertextHeaders; + private final byte[] ciphertextHeaderBytes; + private final byte[] messageId; + private final int frameSize; + private final PrivateKey trailingSignaturePrivateKey; + private final MessageDigest trailingDigest; + private final Signature trailingSig; + private final ContentType contentType; + + public EncryptionMetadata(int frameSize, EncryptionMaterials result) throws AwsCryptoException { + Utils.assertNonNull(result, "result"); + + this.encryptionContext_ = result.getEncryptionContext(); + + this.cryptoAlgo = result.getAlgorithm(); + this.masterKeys = result.getMasterKeys(); + this.keyBlobs = result.getEncryptedDataKeys(); + this.trailingSignaturePrivateKey = result.getTrailingSignatureKey(); + + if (keyBlobs.isEmpty()) { + throw new IllegalArgumentException("No encrypted data keys in materials result"); + } + + if (trailingSignaturePrivateKey != null) { + try { + TrailingSignatureAlgorithm algorithm = TrailingSignatureAlgorithm.forCryptoAlgorithm(cryptoAlgo); + trailingDigest = MessageDigest.getInstance(algorithm.getMessageDigestAlgorithm()); + trailingSig = Signature.getInstance(algorithm.getRawSignatureAlgorithm()); + + trailingSig.initSign(trailingSignaturePrivateKey, com.amazonaws.encryptionsdk.internal.Utils.getSecureRandom()); + } catch (final GeneralSecurityException ex) { + throw new AwsCryptoException(ex); + } + } else { + trailingDigest = null; + trailingSig = null; + } + + // set default values + version = cryptoAlgo.getMessageFormatVersion(); + + // only allow to encrypt with version 1 crypto algorithms + if (version != 1) { + throw new AwsCryptoException( + "Configuration conflict. Cannot encrypt due to CommitmentPolicy " + + CommitmentPolicy.ForbidEncryptAllowDecrypt + + " requiring only non-committed messages. Algorithm ID was " + + cryptoAlgo + + ". See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/troubleshooting-migration.html" + ); + } + + type = CIPHERTEXT_TYPE; + nonceLen = cryptoAlgo.getNonceLen(); + + if (frameSize > 0) { + contentType = ContentType.FRAME; + } else if (frameSize == 0) { + contentType = ContentType.SINGLEBLOCK; + } else { + throw Utils.cannotBeNegative("Frame size"); + } + + final CiphertextHeaders unsignedHeaders = createCiphertextHeaders(contentType, frameSize); + try { + encryptionKey = cryptoAlgo.getEncryptionKeyFromDataKey(result.getCleartextDataKey(), unsignedHeaders); + } catch (final InvalidKeyException ex) { + throw new AwsCryptoException(ex); + } + ciphertextHeaders = signCiphertextHeaders(unsignedHeaders); + ciphertextHeaderBytes = ciphertextHeaders.toByteArray(); + messageId = ciphertextHeaders.getMessageId(); + this.frameSize = frameSize; + } + + public ContentType getContentType() { + return contentType; + } + + /** + * Create ciphertext headers using the instance variables, and the provided content type and + * frame size. + * + * @param contentType + * the content type to set in the ciphertext headers. + * @param frameSize + * the frame size to set in the ciphertext headers. + * @return the bytes containing the ciphertext headers. + */ + private CiphertextHeaders createCiphertextHeaders(final ContentType contentType, final int frameSize) { + // create the ciphertext headers + final byte[] headerNonce = new byte[nonceLen]; + // We use a deterministic IV of zero for the header authentication. + + final byte[] encryptionContextBytes = EncryptionContextSerializer.serialize(encryptionContext_); + final CiphertextHeaders ciphertextHeaders = new CiphertextHeaders( + type, + cryptoAlgo, + encryptionContextBytes, + keyBlobs, + contentType, + frameSize + ); + ciphertextHeaders.setHeaderNonce(headerNonce); + + return ciphertextHeaders; + } + + private CiphertextHeaders signCiphertextHeaders(final CiphertextHeaders unsignedHeaders) { + final byte[] headerFields = unsignedHeaders.serializeAuthenticatedFields(); + final byte[] headerTag = computeHeaderTag(unsignedHeaders.getHeaderNonce(), headerFields); + + unsignedHeaders.setHeaderTag(headerTag); + + return unsignedHeaders; + } + + /** + * Compute the MAC tag of the header bytes using the provided key, nonce, AAD, and crypto + * algorithm identifier. + * + * @param nonce + * the nonce to use in computing the MAC tag. + * @param aad + * the AAD to use in computing the MAC tag. + * @return the bytes containing the computed MAC tag. + */ + private byte[] computeHeaderTag(final byte[] nonce, final byte[] aad) { + final CipherHandler cipherHandler = new CipherHandler(encryptionKey, Cipher.ENCRYPT_MODE, cryptoAlgo); + + return cipherHandler.cipherData(nonce, aad, new byte[0], 0, 0); + } + + public Map getEncryptionContext() { + return encryptionContext_; + } + + public CryptoAlgorithm getCryptoAlgo() { + return cryptoAlgo; + } + + public List getMasterKeys() { + return masterKeys; + } + + public List getKeyBlobs() { + return keyBlobs; + } + + public SecretKey getEncryptionKey() { + return encryptionKey; + } + + public byte getVersion() { + return version; + } + + public CiphertextType getType() { + return type; + } + + public byte getNonceLen() { + return nonceLen; + } + + public CiphertextHeaders getCiphertextHeaders() { + return ciphertextHeaders; + } + + public byte[] getCiphertextHeaderBytes() { + return ciphertextHeaderBytes; + } + + public byte[] getMessageId() { + return messageId; + } + + public int getFrameSize() { + return frameSize; + } + + public PrivateKey getTrailingSignaturePrivateKey() { + return trailingSignaturePrivateKey; + } + + public MessageDigest getTrailingDigest() { + return trailingDigest; + } + + public Signature getTrailingSig() { + return trailingSig; + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameCryptoHandler.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameCryptoHandler.java new file mode 100644 index 0000000000000..dee821e5cdf2d --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameCryptoHandler.java @@ -0,0 +1,240 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.encryption.frame; + +import org.opensearch.common.crypto.CryptoHandler; +import org.opensearch.common.crypto.DecryptedRangedStreamProvider; +import org.opensearch.common.crypto.EncryptedHeaderContentSupplier; +import org.opensearch.common.io.InputStreamContainer; +import org.opensearch.encryption.TrimmingStream; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import com.amazonaws.encryptionsdk.ParsedCiphertext; + +public class FrameCryptoHandler implements CryptoHandler { + private final AwsCrypto awsCrypto; + private final Map encryptionContext; + private final Runnable onClose; + + // package private for tests + private final int FRAME_SIZE = 8 * 1024; + + public FrameCryptoHandler(AwsCrypto awsCrypto, Map encryptionContext, Runnable onClose) { + this.awsCrypto = awsCrypto; + this.encryptionContext = encryptionContext; + this.onClose = onClose; + } + + public int getFrameSize() { + return FRAME_SIZE; + } + + /** + * Initialises metadata store used in encryption. + * @return crypto metadata object constructed with encryption metadata like data key pair, encryption algorithm, etc. + */ + public EncryptionMetadata initEncryptionMetadata() { + return awsCrypto.createCryptoContext(encryptionContext, getFrameSize()); + } + + /** + * Context: This SDK uses Frame encryption which means that encrypted content is composed of frames i.e., a frame + * is the smallest unit of encryption or decryption. + * Due to this in cases where more than one stream is used to produce content, each stream content except the + * last should line up along the frame boundary i.e. there can't be any partial frame. + * Hence, size of each stream except the last, should be exactly divisible by the frame size and therefore, this + * method should be called before committing on the stream size. + * This is not required if number of streams for a content is only 1. + * + * @param encryptionMetadata stateful object for a request consisting of materials required in encryption. + * @param streamSize Size of the stream to be adjusted. + * @return Adjusted size of the stream. + */ + public long adjustContentSizeForPartialEncryption(EncryptionMetadata encryptionMetadata, long streamSize) { + return (streamSize - (streamSize % encryptionMetadata.getFrameSize())) + encryptionMetadata.getFrameSize(); + } + + /** + * Estimate length of the encrypted stream. + * + * @param encryptionMetadata crypto metadata instance + * @param contentLength Size of the raw content + * @return Calculated size of the encrypted stream for the provided raw stream. + */ + public long estimateEncryptedLengthOfEntireContent(EncryptionMetadata encryptionMetadata, long contentLength) { + return encryptionMetadata.getCiphertextHeaderBytes().length + awsCrypto.estimateOutputSizeWithFooter( + encryptionMetadata.getFrameSize(), + encryptionMetadata.getNonceLen(), + encryptionMetadata.getCryptoAlgo().getTagLen(), + contentLength, + encryptionMetadata.getCryptoAlgo() + ); + } + + /** + * Estimate length of the decrypted stream. + * + * @param parsedCiphertext crypto metadata instance + * @param contentLength Size of the encrypted content + * @return Calculated size of the encrypted stream for the provided raw stream. + */ + public long estimateDecryptedLength(ParsedCiphertext parsedCiphertext, long contentLength) { + return awsCrypto.estimateDecryptedSize( + parsedCiphertext.getFrameLength(), + parsedCiphertext.getNonceLength(), + parsedCiphertext.getCryptoAlgoId().getTagLen(), + contentLength - parsedCiphertext.getOffset(), + parsedCiphertext.getCryptoAlgoId() + ); + } + + /** + * Wraps a raw InputStream with encrypting stream + * @param encryptionMetadata consists encryption metadata. + * @param stream Raw InputStream to encrypt + * @return encrypting stream wrapped around raw InputStream. + */ + public InputStreamContainer createEncryptingStream(EncryptionMetadata encryptionMetadata, InputStreamContainer stream) { + return createEncryptingStreamOfPart(encryptionMetadata, stream, 1, 0); + } + + /** + * Provides encrypted stream for a raw stream emitted for a part of content. This method doesn't require streams of + * the content to be provided in sequence and is thread safe. + * Note: This method assumes that all streams except the last stream are of same size. Also, length of the stream + * except the last index must exactly align with frame length. + * + * @param encryptionMetadata stateful object for a request consisting of materials required in encryption. + * @param stream raw stream for which encrypted stream has to be created. + * @param totalStreams Number of streams being used for the entire content. + * @param streamIdx Index of the current stream. + * @return Encrypted stream for the provided raw stream. + */ + public InputStreamContainer createEncryptingStreamOfPart( + EncryptionMetadata encryptionMetadata, + InputStreamContainer stream, + int totalStreams, + int streamIdx + ) { + int frameStartNumber = (int) (stream.getOffset() / getFrameSize()) + 1; + + return awsCrypto.createEncryptingStream(stream, streamIdx, totalStreams, frameStartNumber, encryptionMetadata); + } + + /** + * + * @param encryptedHeaderContentSupplier Supplier used to fetch bytes from source for header creation + * @return parsed encryption metadata object + * @throws IOException if content fetch for header creation fails + */ + public ParsedCiphertext loadEncryptionMetadata(EncryptedHeaderContentSupplier encryptedHeaderContentSupplier) throws IOException { + byte[] encryptedHeader = encryptedHeaderContentSupplier.supply(0, 4095); + return new ParsedCiphertext(encryptedHeader); + } + + /** + * This method accepts an encrypted stream and provides a decrypting wrapper. + * + * @param encryptedStream to be decrypted. + * @return Decrypting wrapper stream + */ + public InputStream createDecryptingStream(InputStream encryptedStream) { + return awsCrypto.createDecryptingStream(encryptedStream); + } + + /** + * Provides trailing signature length if any based on the crypto algorithm used. + * @param encryptionMetadata Context object needed to calculate trailing length. + * @return Trailing signature length + */ + public int getTrailingSignatureLength(EncryptionMetadata encryptionMetadata) { + return awsCrypto.getTrailingSignatureSize(encryptionMetadata.getCryptoAlgo()); + } + + private InputStream createBlockDecryptionStream( + ParsedCiphertext parsedCiphertext, + InputStream inputStream, + long startPosOfRawContent, + long endPosOfRawContent, + long[] encryptedRange + ) { + if (startPosOfRawContent % parsedCiphertext.getFrameLength() != 0 + || (endPosOfRawContent + 1) % parsedCiphertext.getFrameLength() != 0) { + throw new IllegalArgumentException("Start and end positions of the raw content must be aligned with frame length"); + } + int frameStartNumber = (int) (startPosOfRawContent / parsedCiphertext.getFrameLength()) + 1; + long encryptedSize = encryptedRange[1] - encryptedRange[0] + 1; + return awsCrypto.createDecryptingStream(inputStream, encryptedSize, parsedCiphertext, frameStartNumber, false); + } + + /** + * For partial reads of encrypted content, frame based encryption requires the range of content to be adjusted for + * successful decryption. Adjusted range may or may not be same as the provided range. If range is adjusted then + * starting offset of resultant range can be lesser than the starting offset of provided range and end + * offset can be greater than the ending offset of the provided range. + * It provides supplier for creating decrypted stream out of the provided encrypted stream. Decrypted content is + * trimmed down to the desired range with the help of bounded stream. This method assumes that provided encrypted + * stream supplies content for the adjusted range. + * + * @param encryptionMetadata crypto metadata instance consisting of encryption metadata used in encryption. + * @param startPosOfRawContent starting position in the raw/decrypted content + * @param endPosOfRawContent ending position in the raw/decrypted content + * @return stream provider for decrypted stream for the specified range of content including adjusted range + */ + public DecryptedRangedStreamProvider createDecryptingStreamOfRange( + ParsedCiphertext encryptionMetadata, + long startPosOfRawContent, + long endPosOfRawContent + ) { + + long adjustedStartPos = startPosOfRawContent - (startPosOfRawContent % encryptionMetadata.getFrameLength()); + long endPosOverhead = (endPosOfRawContent + 1) % encryptionMetadata.getFrameLength(); + long adjustedEndPos = endPosOverhead == 0 + ? endPosOfRawContent + : (endPosOfRawContent - endPosOverhead + encryptionMetadata.getFrameLength()); + long[] encryptedRange = transformToEncryptedRange(encryptionMetadata, adjustedStartPos, adjustedEndPos); + return new DecryptedRangedStreamProvider(encryptedRange, (encryptedStream) -> { + InputStream decryptedStream = createBlockDecryptionStream( + encryptionMetadata, + encryptedStream, + adjustedStartPos, + adjustedEndPos, + encryptedRange + ); + return new TrimmingStream(adjustedStartPos, adjustedEndPos, startPosOfRawContent, endPosOfRawContent, decryptedStream); + }); + } + + private long[] transformToEncryptedRange(ParsedCiphertext parsedCiphertext, long startPosOfRawContent, long endPosOfRawContent) { + + long startPos = awsCrypto.estimatePartialOutputSize( + parsedCiphertext.getFrameLength(), + parsedCiphertext.getCryptoAlgoId().getNonceLen(), + parsedCiphertext.getCryptoAlgoId().getTagLen(), + startPosOfRawContent + ) + parsedCiphertext.getOffset(); + + long endPos = awsCrypto.estimatePartialOutputSize( + parsedCiphertext.getFrameLength(), + parsedCiphertext.getCryptoAlgoId().getNonceLen(), + parsedCiphertext.getCryptoAlgoId().getTagLen(), + endPosOfRawContent + ) + parsedCiphertext.getOffset(); + + return new long[] { startPos, endPos }; + } + + @Override + public void close() { + onClose.run(); + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameDecryptionHandler.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameDecryptionHandler.java new file mode 100644 index 0000000000000..2612e7608774d --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameDecryptionHandler.java @@ -0,0 +1,319 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.opensearch.encryption.frame; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import java.util.Arrays; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.Constants; +import com.amazonaws.encryptionsdk.internal.CryptoHandler; +import com.amazonaws.encryptionsdk.internal.ProcessingSummary; +import com.amazonaws.encryptionsdk.model.CipherFrameHeaders; + +/** + * The frame decryption handler is a subclass of the decryption handler and + * thereby provides an implementation of the Cryptography handler. + * + *

+ * It implements methods for decrypting content that was encrypted and stored in + * frames. + */ +class FrameDecryptionHandler implements CryptoHandler { + private final SecretKey decryptionKey_; + private final CryptoAlgorithm cryptoAlgo_; + private final CipherHandler cipherHandler_; + private final byte[] messageId_; + + private final short nonceLen_; + + private CipherFrameHeaders currentFrameHeaders_; + private final int frameSize_; + private long frameNumber_; + + boolean complete_ = false; + private byte[] unparsedBytes_ = new byte[0]; + + /** + * Construct a decryption handler for decrypting bytes stored in frames. + * + */ + FrameDecryptionHandler( + final SecretKey decryptionKey, + final short nonceLen, + final CryptoAlgorithm cryptoAlgo, + final byte[] messageId, + final int frameLen, + final int frameStartNumber + ) { + decryptionKey_ = decryptionKey; + nonceLen_ = nonceLen; + cryptoAlgo_ = cryptoAlgo; + messageId_ = messageId; + frameSize_ = frameLen; + cipherHandler_ = new CipherHandler(decryptionKey_, Cipher.DECRYPT_MODE, cryptoAlgo_); + frameNumber_ = frameStartNumber; + } + + /** + * Decrypt the ciphertext bytes containing content encrypted using frames and put the plaintext + * bytes into out. + * + *

+ * It decrypts by performing the following operations: + *

    + *
  1. parse the ciphertext headers
  2. + *
  3. parse the ciphertext until encrypted content in a frame is available
  4. + *
  5. decrypt the encrypted content
  6. + *
  7. return decrypted bytes as output
  8. + *
+ * + * @param in + * the input byte array. + * @param out + * the output buffer the decrypted plaintext bytes go into. + * @param outOff + * the offset into the output byte array the decrypted data starts at. + * @return the number of bytes written to out and processed + * @throws AwsCryptoException + * if the content type found in the headers is not of frame type. + */ + @Override + public ProcessingSummary processBytes(final byte[] in, final int off, final int len, final byte[] out, final int outOff) + throws AwsCryptoException { + + if (complete_) { + throw new AwsCryptoException("Ciphertext has already been processed."); + } + + final long totalBytesToParse = unparsedBytes_.length + (long) len; + if (totalBytesToParse > Integer.MAX_VALUE) { + throw new AwsCryptoException("Integer overflow of the total bytes to parse and decrypt occured."); + } + + final byte[] bytesToParse = new byte[(int) totalBytesToParse]; + // If there were previously unparsed bytes, add them as the first + // set of bytes to be parsed in this call. + System.arraycopy(unparsedBytes_, 0, bytesToParse, 0, unparsedBytes_.length); + System.arraycopy(in, off, bytesToParse, unparsedBytes_.length, len); + + int actualOutLen = 0; + int totalParsedBytes = 0; + + // Parse available bytes. Stop parsing when there aren't enough + // bytes to complete parsing: + // - the ciphertext headers + // - the cipher frame + while (!complete_ && totalParsedBytes < bytesToParse.length) { + if (currentFrameHeaders_ == null) { + currentFrameHeaders_ = new CipherFrameHeaders(); + currentFrameHeaders_.setNonceLength(nonceLen_); + if (frameSize_ == 0) { + // if frame size in ciphertext headers is 0, the frame size + // will need to be parsed in individual frame headers. + currentFrameHeaders_.includeFrameSize(true); + } + } + + totalParsedBytes += currentFrameHeaders_.deserialize(bytesToParse, totalParsedBytes); + + // if we have all frame fields, process the encrypted content. + if (currentFrameHeaders_.isComplete() == true) { + int protectedContentLen = -1; + if (currentFrameHeaders_.isFinalFrame()) { + protectedContentLen = currentFrameHeaders_.getFrameContentLength(); + + // The final frame should not be able to exceed the frameLength + if (frameSize_ > 0 && protectedContentLen > frameSize_) { + throw new BadCiphertextException("Final frame length exceeds frame length."); + } + } else { + protectedContentLen = frameSize_; + } + + // include the tag which is added by the underlying cipher. + protectedContentLen += cryptoAlgo_.getTagLen(); + + if ((bytesToParse.length - totalParsedBytes) < protectedContentLen) { + // if we don't have all of the encrypted bytes, break + // until they become available. + break; + } + + final byte[] bytesToDecrypt_ = Arrays.copyOfRange(bytesToParse, totalParsedBytes, totalParsedBytes + protectedContentLen); + totalParsedBytes += protectedContentLen; + + if (frameNumber_ == com.amazonaws.encryptionsdk.internal.Constants.MAX_FRAME_NUMBER) { + throw new BadCiphertextException("Frame number exceeds the maximum allowed value."); + } + + final byte[] decryptedBytes = decryptContent(bytesToDecrypt_, 0, bytesToDecrypt_.length); + + System.arraycopy(decryptedBytes, 0, out, (outOff + actualOutLen), decryptedBytes.length); + actualOutLen += decryptedBytes.length; + frameNumber_++; + + complete_ = currentFrameHeaders_.isFinalFrame(); + // reset frame headers as we are done processing current frame. + currentFrameHeaders_ = null; + } else { + // if there aren't enough bytes to parse cipher frame, + // we can't continue parsing. + break; + } + } + + if (!complete_) { + // buffer remaining bytes for parsing in the next round. + unparsedBytes_ = Arrays.copyOfRange(bytesToParse, totalParsedBytes, bytesToParse.length); + return new ProcessingSummary(actualOutLen, len); + } else { + final ProcessingSummary result = new ProcessingSummary(actualOutLen, totalParsedBytes - unparsedBytes_.length); + unparsedBytes_ = new byte[0]; + return result; + } + } + + /** + * Finish processing of the bytes. This function does nothing since the + * final frame will be processed and decrypted in processBytes(). + * + * @param out + * space for any resulting output data. + * @param outOff + * offset into out to start copying the data at. + * @return + * 0 + */ + @Override + public int doFinal(final byte[] out, final int outOff) { + if (!complete_) { + throw new BadCiphertextException("Unable to process entire ciphertext."); + } + + return 0; + } + + /** + * Return the size of the output buffer required for a processBytes plus a + * doFinal with an input of inLen bytes. + * + * @param inLen + * the length of the input. + * @return + * the space required to accommodate a call to processBytes and + * doFinal with len bytes of input. + */ + @Override + public int estimateOutputSize(final int inLen) { + int outSize = 0; + + final int totalBytesToDecrypt = unparsedBytes_.length + inLen; + if (totalBytesToDecrypt > 0) { + int frames = totalBytesToDecrypt / frameSize_; + frames += 1; // add one for final frame which might be < frame size. + outSize += (frameSize_ * frames); + } + + return outSize; + } + + public static long estimateDecryptedSize(long encryptedSize, int frameSize, int nonceLen, int tagLenBytes) { + // Calculate the size of sequence number for the last frame + long lastFrameSeqNumberSize = (Integer.SIZE / Byte.SIZE); + + // Calculate the size of the final frame size + long finalFrameSizeSize = (Integer.SIZE / Byte.SIZE); + + // Calculate the total size of header overhead for the last frame + long lastFrameHeaderOverhead = lastFrameSeqNumberSize + finalFrameSizeSize; + + // Calculate the number of frames + long frames = (encryptedSize - lastFrameHeaderOverhead) / (frameSize + nonceLen + tagLenBytes + (Integer.SIZE / Byte.SIZE)) + 1; + + // Calculate the size of the actual content in frames + long contentSizeWithoutLastFrame = (frames - 1) * frameSize; + + // Calculate the sequence number size for all frames + long seqNumberSize = frames * (Integer.SIZE / Byte.SIZE); + + // Calculate the total size of header overhead for all frames + long headerOverhead = (nonceLen + tagLenBytes) * frames + seqNumberSize; + + // Calculate the size of the last frame content + long lastFrameSize = encryptedSize - contentSizeWithoutLastFrame - headerOverhead - lastFrameHeaderOverhead; + + return contentSizeWithoutLastFrame + lastFrameSize; + } + + @Override + public int estimatePartialOutputSize(int inLen) { + return estimateOutputSize(inLen); + } + + @Override + public int estimateFinalOutputSize() { + return 0; + } + + /** + * Returns the plaintext bytes of the encrypted content. + * + * @param input + * the input bytes containing the content + * @param off + * the offset into the input array where the data to be decrypted + * starts. + * @param len + * the number of bytes to be decrypted. + * @return + * the plaintext bytes of the encrypted content. + * @throws BadCiphertextException + * if the bytes do not decrypt correctly. + */ + private byte[] decryptContent(final byte[] input, final int off, final int len) throws BadCiphertextException { + final byte[] nonce = currentFrameHeaders_.getNonce(); + + byte[] contentAad = null; + if (currentFrameHeaders_.isFinalFrame() == true) { + contentAad = Utils.generateContentAad( + messageId_, + Constants.FINAL_FRAME_STRING_ID, + (int) frameNumber_, + currentFrameHeaders_.getFrameContentLength() + ); + } else { + contentAad = Utils.generateContentAad(messageId_, Constants.FRAME_STRING_ID, (int) frameNumber_, frameSize_); + } + + return cipherHandler_.cipherData(nonce, contentAad, input, off, len); + } + + @Override + public boolean isComplete() { + return complete_; + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameEncryptionHandler.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameEncryptionHandler.java new file mode 100644 index 0000000000000..e5091f8e5caf8 --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/FrameEncryptionHandler.java @@ -0,0 +1,376 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.opensearch.encryption.frame; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.Constants; +import com.amazonaws.encryptionsdk.internal.CryptoHandler; +import com.amazonaws.encryptionsdk.internal.ProcessingSummary; +import com.amazonaws.encryptionsdk.model.CipherFrameHeaders; + +/** + * The frame encryption handler is a subclass of the encryption handler and + * thereby provides an implementation of the Cryptography handler. + * + *

+ * It implements methods for encrypting content and storing the encrypted bytes + * in frames. + */ +class FrameEncryptionHandler implements CryptoHandler { + private final SecretKey encryptionKey_; + private final CryptoAlgorithm cryptoAlgo_; + private final CipherHandler cipherHandler_; + private final int nonceLen_; + private final byte[] messageId_; + private final int frameSize_; + private final int tagLenBytes_; + + private long frameNumber_; + private boolean isFinalFrame_; + + private final byte[] bytesToFrame_; + private int bytesToFrameLen_; + private boolean complete_ = false; + + /** + * Construct an encryption handler for encrypting bytes and storing them in + * frames. + */ + public FrameEncryptionHandler( + final SecretKey encryptionKey, + final int nonceLen, + final CryptoAlgorithm cryptoAlgo, + final byte[] messageId, + final int frameSize, + final int frameStartNumber + ) { + encryptionKey_ = encryptionKey; + cryptoAlgo_ = cryptoAlgo; + nonceLen_ = nonceLen; + messageId_ = messageId.clone(); + frameSize_ = frameSize; + tagLenBytes_ = cryptoAlgo_.getTagLen(); + bytesToFrame_ = new byte[frameSize_]; + bytesToFrameLen_ = 0; + cipherHandler_ = new CipherHandler(encryptionKey_, Cipher.ENCRYPT_MODE, cryptoAlgo_); + frameNumber_ = frameStartNumber; + } + + /** + * Encrypt a block of bytes from in putting the plaintext result into out. + * + *

+ * It encrypts by performing the following operations: + *

    + *
  1. determine the size of encrypted content that can fit into current frame
  2. + *
  3. call processBytes() of the underlying cipher to do corresponding cryptographic encryption + * of plaintext
  4. + *
  5. check if current frame is fully filled using the processed bytes, write current frame to + * the output being returned.
  6. + *
+ * + * @param in + * the input byte array. + * @param out + * the output buffer the encrypted bytes go into. + * @param outOff + * the offset into the output byte array the encrypted data starts at. + * @return the number of bytes written to out and processed + */ + @Override + public ProcessingSummary processBytes(final byte[] in, final int off, final int len, final byte[] out, final int outOff) + throws BadCiphertextException { + int actualOutLen = 0; + + int size = len; + int offset = off; + while (size > 0) { + final int currentFrameCapacity = frameSize_ - bytesToFrameLen_; + // bind size to the capacity of the current frame + size = Math.min(currentFrameCapacity, size); + + System.arraycopy(in, offset, bytesToFrame_, bytesToFrameLen_, size); + bytesToFrameLen_ += size; + + // check if there is enough bytes to create a frame + if (bytesToFrameLen_ == frameSize_) { + actualOutLen += writeEncryptedFrame(bytesToFrame_, 0, bytesToFrameLen_, out, outOff + actualOutLen); + + // reset buffer len as a new frame is created in next iteration + bytesToFrameLen_ = 0; + } + + // update offset by the size of bytes being encrypted. + offset += size; + // update size to the remaining bytes starting at offset. + size = len - offset; + } + + return new ProcessingSummary(actualOutLen, len); + } + + /** + * Finish processing of the bytes by writing out the ciphertext or final + * frame if framing. + * + * @param out + * space for any resulting output data. + * @param outOff + * offset into out to start copying the data at. + * @return + * number of bytes written into out. + */ + @Override + public int doFinal(final byte[] out, final int outOff) throws BadCiphertextException { + isFinalFrame_ = true; + complete_ = true; + return writeEncryptedFrame(bytesToFrame_, 0, bytesToFrameLen_, out, outOff); + } + + /** + * Return the size of the output buffer required for a processBytes plus a + * doFinal with an input of inLen bytes. + * + * @param inLen + * the length of the input. + * @return + * the space required to accommodate a call to processBytes and + * doFinal with len bytes of input. + */ + @Override + public int estimateOutputSize(final int inLen) { + // include any bytes held for inclusion in a subsequent frame + int totalContent = bytesToFrameLen_ + inLen; + return (int) estimatePartialSizeFromMetadata(totalContent, true, frameSize_, nonceLen_, tagLenBytes_); + } + + public static long estimatePartialSizeFromMetadata( + long totalContent, + boolean includeLastFrame, + int frameSize, + int nonceLen, + int tagLenBytes + ) { + // compute the size of the frames that will be constructed + long frames = totalContent / frameSize; + long outSize = (frameSize * frames); + + // account for remaining data that will need a new frame. + final long leftover = totalContent % frameSize; + outSize += leftover; + // even if leftover is 0, there will be a final frame. + if (includeLastFrame || leftover > 0) { + frames += 1; + } + + /* + * Calculate overhead of frame headers. + */ + // nonce and MAC tag. + outSize += frames * (nonceLen + tagLenBytes); + + // sequence number for all frames + outSize += frames * (Integer.SIZE / Byte.SIZE); + + if (includeLastFrame) { + // sequence number end for final frame + outSize += Integer.SIZE / Byte.SIZE; + + // integer for storing final frame size + outSize += Integer.SIZE / Byte.SIZE; + } + + return outSize; + } + + @Override + public int estimatePartialOutputSize(int inLen) { + int outSize = 0; + int frames = 0; + + // include any bytes held for inclusion in a subsequent frame + int totalContent = bytesToFrameLen_; + if (inLen >= 0) { + totalContent += inLen; + } + + // compute the size of the frames that will be constructed + frames = totalContent / frameSize_; + outSize += (frameSize_ * frames); + + /* + * Calculate overhead of frame headers. + */ + // nonce and MAC tag. + outSize += frames * (nonceLen_ + tagLenBytes_); + + // sequence number for all frames + outSize += frames * (Integer.SIZE / Byte.SIZE); + + return outSize; + } + + @Override + public int estimateFinalOutputSize() { + int outSize = 0; + int frames = 0; + + // include any bytes held for inclusion in a subsequent frame + int totalContent = bytesToFrameLen_; + + // compute the size of the frames that will be constructed + frames = totalContent / frameSize_; + outSize += (frameSize_ * frames); + + // account for remaining data that will need a new frame. + final int leftover = totalContent % frameSize_; + outSize += leftover; + // even if leftover is 0, there will be a final frame. + frames += 1; + + /* + * Calculate overhead of frame headers. + */ + // nonce and MAC tag. + outSize += frames * (nonceLen_ + tagLenBytes_); + + // sequence number for all frames + outSize += frames * (Integer.SIZE / Byte.SIZE); + + // sequence number end for final frame + outSize += Integer.SIZE / Byte.SIZE; + + // integer for storing final frame size + outSize += Integer.SIZE / Byte.SIZE; + + return outSize; + } + + /** + * We encrypt the bytes, create the headers for the block, and assemble the + * frame containing the headers and the encrypted bytes. + * @param out + * the output buffer the encrypted bytes go into. + * @param outOff + * the offset into the output byte array the encrypted data + * starts at. + * @return + * the number of bytes written to out. + * @throws BadCiphertextException + * thrown by the underlying cipher handler. + * @throws AwsCryptoException + * if frame number exceeds the maximum allowed value. + */ + private int writeEncryptedFrame(final byte[] input, final int off, final int len, final byte[] out, final int outOff) + throws BadCiphertextException, AwsCryptoException { + if (frameNumber_ > com.amazonaws.encryptionsdk.internal.Constants.MAX_FRAME_NUMBER + // Make sure we have the appropriate flag set for the final frame; we don't want to accept + // non-final-frame data when there won't be a subsequent frame for it to go into. + || (frameNumber_ == com.amazonaws.encryptionsdk.internal.Constants.MAX_FRAME_NUMBER && !isFinalFrame_)) { + throw new AwsCryptoException("Frame number exceeded the maximum allowed value."); + } + + if (out.length == 0) { + return 0; + } + + int outLen = 0; + + byte[] contentAad; + if (isFinalFrame_ == true) { + contentAad = Utils.generateContentAad( + messageId_, + com.amazonaws.encryptionsdk.internal.Constants.FINAL_FRAME_STRING_ID, + (int) frameNumber_, + len + ); + } else { + contentAad = Utils.generateContentAad( + messageId_, + com.amazonaws.encryptionsdk.internal.Constants.FRAME_STRING_ID, + (int) frameNumber_, + frameSize_ + ); + } + + final byte[] nonce = getNonce(); + + final byte[] encryptedBytes = cipherHandler_.cipherData(nonce, contentAad, input, off, len); + + // create the cipherblock headers now for the encrypted data + final int encryptedContentLen = encryptedBytes.length - tagLenBytes_; + final CipherFrameHeaders cipherFrameHeaders = new CipherFrameHeaders((int) frameNumber_, nonce, encryptedContentLen, isFinalFrame_); + final byte[] cipherFrameHeaderBytes = cipherFrameHeaders.toByteArray(); + + // assemble the headers and the encrypted bytes into a single block + System.arraycopy(cipherFrameHeaderBytes, 0, out, outOff + outLen, cipherFrameHeaderBytes.length); + outLen += cipherFrameHeaderBytes.length; + System.arraycopy(encryptedBytes, 0, out, outOff + outLen, encryptedBytes.length); + outLen += encryptedBytes.length; + + frameNumber_++; + + return outLen; + } + + private byte[] getNonce() { + /* + * To mitigate the risk of IVs colliding within the same message, we use deterministic IV generation within a + * message. + */ + + if (frameNumber_ < 1) { + // This should never happen - however, since we use a "frame number zero" IV elsewhere (for header auth), + // we must be sure that we don't reuse it here. + throw new IllegalStateException("Illegal frame number"); + } + + if ((int) frameNumber_ == Constants.ENDFRAME_SEQUENCE_NUMBER && !isFinalFrame_) { + throw new IllegalStateException("Too many frames"); + } + + final byte[] nonce = new byte[nonceLen_]; + + ByteBuffer buf = ByteBuffer.wrap(nonce); + buf.order(ByteOrder.BIG_ENDIAN); + // We technically only allocate the low 32 bits for the frame number, and the other bits are defined to be + // zero. However, since MAX_FRAME_NUMBER is 2^32-1, the high-order four bytes of the long will be zero, so the + // big-endian representation will also have zeros in that position. + Utils.position(buf, buf.limit() - Long.BYTES); + buf.putLong(frameNumber_); + + return nonce; + } + + @Override + public boolean isComplete() { + return complete_; + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/frame/Utils.java b/modules/crypto/src/main/java/org/opensearch/encryption/frame/Utils.java new file mode 100644 index 0000000000000..1878089f8abfd --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/frame/Utils.java @@ -0,0 +1,106 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.opensearch.encryption.frame; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +/** Internal utility methods. */ +public final class Utils { + // SecureRandom objects can both be expensive to initialize and incur synchronization costs. + // This allows us to minimize both initializations and keep SecureRandom usage thread local + // to avoid lock contention. + private static final ThreadLocal LOCAL_RANDOM = new ThreadLocal() { + @Override + protected SecureRandom initialValue() { + final SecureRandom rnd = new SecureRandom(); + rnd.nextBoolean(); // Force seeding + return rnd; + } + }; + + private Utils() { + // Prevent instantiation + } + + /** + * Throws {@link NullPointerException} with message {@code paramName} if {@code object} is null. + * + * @param object value to be null-checked + * @param paramName message for the potential {@link NullPointerException} + * @return {@code object} + * @throws NullPointerException if {@code object} is null + * @param Type of object on which null check is to be performed + */ + public static T assertNonNull(final T object, final String paramName) throws NullPointerException { + if (object == null) { + throw new NullPointerException(paramName + " must not be null"); + } + return object; + } + + public static SecureRandom getSecureRandom() { + return LOCAL_RANDOM.get(); + } + + /** + * Generate the AAD bytes to use when encrypting/decrypting content. The generated AAD is a block + * of bytes containing the provided message identifier, the string identifier, the sequence + * number, and the length of the content. + * + * @param messageId the unique message identifier for the ciphertext. + * @param idString the string describing the type of content processed. + * @param seqNum the sequence number. + * @param len the length of the content. + * @return the bytes containing the generated AAD. + */ + static byte[] generateContentAad(final byte[] messageId, final String idString, final int seqNum, final long len) { + final byte[] idBytes = idString.getBytes(StandardCharsets.UTF_8); + final int aadLen = messageId.length + idBytes.length + Integer.SIZE / Byte.SIZE + Long.SIZE / Byte.SIZE; + final ByteBuffer aad = ByteBuffer.allocate(aadLen); + + aad.put(messageId); + aad.put(idBytes); + aad.putInt(seqNum); + aad.putLong(len); + + return aad.array(); + } + + public static IllegalArgumentException cannotBeNegative(String field) { + return new IllegalArgumentException(field + " cannot be negative"); + } + + /** + * Equivalent to calling {@link ByteBuffer#position(int)} but in a manner which is safe when + * compiled on Java 9 or newer but used on Java 8 or older. + * @param buff on which position needs to be set. + * @param newPosition New position to be set + * @return {@link ByteBuffer} object with new position set. + */ + public static ByteBuffer position(final ByteBuffer buff, final int newPosition) { + ((Buffer) buff).position(newPosition); + return buff; + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/keyprovider/CryptoMasterKey.java b/modules/crypto/src/main/java/org/opensearch/encryption/keyprovider/CryptoMasterKey.java new file mode 100644 index 0000000000000..6f014c9b4d99b --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/keyprovider/CryptoMasterKey.java @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.encryption.keyprovider; + +import org.opensearch.common.crypto.DataKeyPair; +import org.opensearch.common.crypto.MasterKeyProvider; + +import javax.crypto.spec.SecretKeySpec; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Map; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.DataKey; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; + +public class CryptoMasterKey extends MasterKey implements Closeable { + private final MasterKeyProvider keyProvider; + private final String keyProviderName; + private final String cryptoAlgorithm; + + public CryptoMasterKey(MasterKeyProvider keyProvider, String keyProviderName, String cryptoAlgorithm) { + this.keyProvider = keyProvider; + this.keyProviderName = keyProviderName; + this.cryptoAlgorithm = cryptoAlgorithm; + } + + @Override + public String getProviderId() { + return keyProviderName; + } + + @Override + public String getKeyId() { + return keyProvider.getKeyId(); + } + + @Override + public DataKey generateDataKey(CryptoAlgorithm algorithm, Map encryptionContext) { + DataKeyPair dataKeyPairResponse = keyProvider.generateDataPair(); + final SecretKeySpec key = new SecretKeySpec(dataKeyPairResponse.getRawKey(), cryptoAlgorithm); + return new DataKey<>(key, dataKeyPairResponse.getEncryptedKey(), getKeyId().getBytes(StandardCharsets.UTF_8), this); + } + + @Override + public DataKey encryptDataKey(CryptoAlgorithm algorithm, Map encryptionContext, DataKey dataKey) { + throw new UnsupportedOperationException("Multiple data-key encryption is not supported."); + } + + @Override + public DataKey decryptDataKey( + CryptoAlgorithm algorithm, + Collection encryptedDataKeys, + Map encryptionContext + ) throws AwsCryptoException { + if (encryptedDataKeys == null || encryptedDataKeys.isEmpty()) { + throw new IllegalArgumentException("No encrypted data key passed for decryption."); + } + EncryptedDataKey encryptedDataKey = encryptedDataKeys.iterator().next(); + final String keyId = new String(encryptedDataKey.getProviderInformation(), StandardCharsets.UTF_8); + if (!this.getKeyId().equals(keyId)) { + throw new IllegalArgumentException("Invalid provider info present in encrypted key."); + } + + byte[] encryptedKey = encryptedDataKey.getEncryptedDataKey(); + byte[] rawKey = keyProvider.decryptKey(encryptedKey); + return new DataKey<>(new SecretKeySpec(rawKey, cryptoAlgorithm), encryptedKey, keyId.getBytes(StandardCharsets.UTF_8), this); + } + + @Override + public void close() throws IOException { + keyProvider.close(); + } +} diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/keyprovider/package-info.java b/modules/crypto/src/main/java/org/opensearch/encryption/keyprovider/package-info.java new file mode 100644 index 0000000000000..611b095a54250 --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/keyprovider/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Key provider package specific to encryption sdk + */ +package org.opensearch.encryption.keyprovider; diff --git a/modules/crypto/src/main/java/org/opensearch/encryption/package-info.java b/modules/crypto/src/main/java/org/opensearch/encryption/package-info.java new file mode 100644 index 0000000000000..1fa008797ce87 --- /dev/null +++ b/modules/crypto/src/main/java/org/opensearch/encryption/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Crypto plugin to for encryption and decryption use cases. + */ +package org.opensearch.encryption; diff --git a/modules/crypto/src/test/java/org/opensearch/encryption/CryptoModulePluginTests.java b/modules/crypto/src/test/java/org/opensearch/encryption/CryptoModulePluginTests.java new file mode 100644 index 0000000000000..dd83acd8feb3a --- /dev/null +++ b/modules/crypto/src/test/java/org/opensearch/encryption/CryptoModulePluginTests.java @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.encryption; + +import org.opensearch.common.crypto.CryptoHandler; +import org.opensearch.common.crypto.MasterKeyProvider; +import org.opensearch.test.OpenSearchTestCase; + +import java.util.Collections; + +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CryptoModulePluginTests extends OpenSearchTestCase { + + private final CryptoModulePlugin cryptoModulePlugin = new CryptoModulePlugin(); + + public void testGetOrCreateCryptoHandler() { + MasterKeyProvider mockKeyProvider = mock(MasterKeyProvider.class); + when(mockKeyProvider.getEncryptionContext()).thenReturn(Collections.emptyMap()); + + CryptoHandler cryptoHandler = cryptoModulePlugin.getOrCreateCryptoHandler( + mockKeyProvider, + "keyProviderName", + "keyProviderType", + () -> {} + ); + + assertNotNull(cryptoHandler); + } + + public void testCreateCryptoProvider() { + CachingCryptoMaterialsManager mockMaterialsManager = mock(CachingCryptoMaterialsManager.class); + MasterKeyProvider mockKeyProvider = mock(MasterKeyProvider.class); + when(mockKeyProvider.getEncryptionContext()).thenReturn(Collections.emptyMap()); + + CryptoHandler cryptoHandler = cryptoModulePlugin.createCryptoHandler( + "ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256", + mockMaterialsManager, + mockKeyProvider, + () -> {} + ); + + assertNotNull(cryptoHandler); + } + + public void testCreateMaterialsManager() { + MasterKeyProvider mockKeyProvider = mock(MasterKeyProvider.class); + when(mockKeyProvider.getEncryptionContext()).thenReturn(Collections.emptyMap()); + + CachingCryptoMaterialsManager materialsManager = cryptoModulePlugin.createMaterialsManager( + mockKeyProvider, + "keyProviderName", + "ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384" + ); + + assertNotNull(materialsManager); + } +} diff --git a/modules/crypto/src/test/java/org/opensearch/encryption/MockKeyProvider.java b/modules/crypto/src/test/java/org/opensearch/encryption/MockKeyProvider.java new file mode 100644 index 0000000000000..a5e74534ef32b --- /dev/null +++ b/modules/crypto/src/test/java/org/opensearch/encryption/MockKeyProvider.java @@ -0,0 +1,109 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.encryption; + +import javax.crypto.spec.SecretKeySpec; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Map; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.DataKey; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; + +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class MockKeyProvider extends MasterKey { + + private static final String keyId = "test-key-id"; + + public static byte[] loadFile(String file) { + byte[] content; + try { + InputStream in = MockKeyProvider.class.getResourceAsStream(file); + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + for (String line; (line = bufferedReader.readLine()) != null;) { + stringBuilder.append(line); + } + content = stringBuilder.toString().getBytes(StandardCharsets.UTF_8); + } catch (Exception e) { + throw new IllegalArgumentException("File " + file + " cannot be read correctly."); + } + String text = new String(content, StandardCharsets.UTF_8); + + String[] byteValues = text.substring(1, text.length() - 1).split(","); + byte[] bytes = new byte[byteValues.length]; + + for (int i = 0, len = bytes.length; i < len; i++) { + bytes[i] = Byte.parseByte(byteValues[i].trim()); + } + + return bytes; + } + + private static final byte[] rawKey = loadFile("/raw_key"); + private static final byte[] encryptedKey = loadFile("/encrypted_key"); + + @Override + public String getProviderId() { + return "sample-provider-id"; + } + + @Override + public String getKeyId() { + return "Sample-key-id"; + } + + @Override + public DataKey encryptDataKey(CryptoAlgorithm algorithm, Map encryptionContext, DataKey dataKey) { + throw new UnsupportedOperationException("Multiple data-key encryption is not supported."); + } + + @Override + public DataKey generateDataKey(CryptoAlgorithm algorithm, Map encryptionContext) { + final SecretKeySpec key = new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()); + return new DataKey(key, encryptedKey, getKeyId().getBytes(StandardCharsets.UTF_8), this); + } + + @Override + public DataKey decryptDataKey(CryptoAlgorithm algorithm, Collection collection, Map encryptionContext) + throws UnsupportedProviderException, AwsCryptoException { + return new DataKey<>( + new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), + encryptedKey, + keyId.getBytes(StandardCharsets.UTF_8), + this + ); + } + + static class DataKeyPair { + private final byte[] rawKey; + private final byte[] encryptedKey; + + public DataKeyPair(byte[] rawKey, byte[] encryptedKey) { + this.rawKey = rawKey; + this.encryptedKey = encryptedKey; + } + + public byte[] getRawKey() { + return this.rawKey; + } + + public byte[] getEncryptedKey() { + return this.encryptedKey; + } + } + +} diff --git a/modules/crypto/src/test/java/org/opensearch/encryption/NoOpCryptoHandlerTests.java b/modules/crypto/src/test/java/org/opensearch/encryption/NoOpCryptoHandlerTests.java new file mode 100644 index 0000000000000..65617a8479eac --- /dev/null +++ b/modules/crypto/src/test/java/org/opensearch/encryption/NoOpCryptoHandlerTests.java @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.encryption; + +import org.opensearch.common.crypto.DecryptedRangedStreamProvider; +import org.opensearch.common.crypto.EncryptedHeaderContentSupplier; +import org.opensearch.common.io.InputStreamContainer; +import org.opensearch.test.OpenSearchTestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class NoOpCryptoHandlerTests extends OpenSearchTestCase { + + public void testInitEncryptionMetadata() { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + Object encryptionMetadata = cryptoProvider.initEncryptionMetadata(); + assertNotNull(encryptionMetadata); + } + + public void testAdjustContentSizeForPartialEncryption() { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + long originalSize = 1000L; + long adjustedSize = cryptoProvider.adjustContentSizeForPartialEncryption(new Object(), originalSize); + assertEquals(originalSize, adjustedSize); + } + + public void testEstimateEncryptedLengthOfEntireContent() { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + long originalSize = 2000L; + long estimatedSize = cryptoProvider.estimateEncryptedLengthOfEntireContent(new Object(), originalSize); + assertEquals(originalSize, estimatedSize); + } + + public void testEstimateDecryptedLength() { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + long originalSize = 1500L; + long estimatedSize = cryptoProvider.estimateDecryptedLength(new Object(), originalSize); + assertEquals(originalSize, estimatedSize); + } + + public void testCreateEncryptingStream() { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + InputStreamContainer inputStream = randomStream(); + InputStreamContainer encryptedStream = cryptoProvider.createEncryptingStream(new Object(), inputStream); + assertEquals(inputStream, encryptedStream); + } + + public void testCreateEncryptingStreamOfPart() { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + InputStreamContainer inputStream = randomStream(); + InputStreamContainer encryptedStream = cryptoProvider.createEncryptingStreamOfPart(new Object(), inputStream, 2, 1); + assertEquals(inputStream, encryptedStream); + } + + private InputStreamContainer randomStream() { + byte[] bytes = randomAlphaOfLength(10).getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + int offset = randomIntBetween(0, bytes.length - 1); + return new InputStreamContainer(byteArrayInputStream, bytes.length, offset); + } + + public void testLoadEncryptionMetadata() throws IOException { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + EncryptedHeaderContentSupplier supplier = (start, length) -> { throw new UnsupportedOperationException("Not implemented"); }; + Object encryptionMetadata = cryptoProvider.loadEncryptionMetadata(supplier); + assertNotNull(encryptionMetadata); + } + + public void testCreateDecryptingStream() { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + InputStream encryptedStream = randomStream().getInputStream(); + InputStream decryptedStream = cryptoProvider.createDecryptingStream(encryptedStream); + assertEquals(encryptedStream, decryptedStream); + } + + public void testCreateDecryptingStreamOfRange() { + NoOpCryptoHandler cryptoProvider = new NoOpCryptoHandler(); + Object cryptoContext = new Object(); + long startPos = 0L; + long endPos = 100L; + DecryptedRangedStreamProvider streamProvider = cryptoProvider.createDecryptingStreamOfRange(cryptoContext, startPos, endPos); + assertNotNull(streamProvider); + InputStream stream = randomStream().getInputStream(); + InputStream decryptedStream = streamProvider.getDecryptedStreamProvider().apply(stream); // Replace with your encrypted input stream + assertEquals(stream, decryptedStream); + } +} diff --git a/modules/crypto/src/test/java/org/opensearch/encryption/TrimmingStreamTests.java b/modules/crypto/src/test/java/org/opensearch/encryption/TrimmingStreamTests.java new file mode 100644 index 0000000000000..f0d957d81e1e1 --- /dev/null +++ b/modules/crypto/src/test/java/org/opensearch/encryption/TrimmingStreamTests.java @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.encryption; + +import org.opensearch.test.OpenSearchTestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class TrimmingStreamTests extends OpenSearchTestCase { + + static class ReadCountInputStreamTest extends ByteArrayInputStream { + + public ReadCountInputStreamTest(byte[] buf) { + super(buf); + } + + public int getPos() { + return pos; + } + } + + public void testReadInRange() throws IOException { + byte[] data = generateRandomData(100); + ReadCountInputStreamTest input = new ReadCountInputStreamTest(data); + + long sourceStart = generateRandomValue(0, 80); + long sourceEnd = generateRandomValue(sourceStart, 99); + long targetStart = generateRandomValue(sourceStart, sourceEnd); + long targetEnd = generateRandomValue(targetStart, sourceEnd); + + TrimmingStream trimmingStream = new TrimmingStream(sourceStart, sourceEnd, targetStart, targetEnd, input); + + byte[] result = new byte[(int) (sourceEnd - sourceStart + 1)]; + int bytesRead = trimmingStream.read(result, 0, result.length); + + long expectedBytesRead = targetEnd - targetStart + 1; + assertEquals(expectedBytesRead, bytesRead); + assertEquals(sourceEnd - sourceStart + 1, input.getPos()); + } + + public void testReadOutsideRange() throws IOException { + byte[] data = generateRandomData(100); + ReadCountInputStreamTest input = new ReadCountInputStreamTest(data); + + long sourceStart = generateRandomValue(0, 80); + long sourceEnd = generateRandomValue(sourceStart, 99); + long targetStart = generateRandomValue(sourceStart, sourceEnd); + long targetEnd = generateRandomValue(targetStart, sourceEnd); + + TrimmingStream trimmingStream = new TrimmingStream(sourceStart, sourceEnd, targetStart, targetEnd, input); + + byte[] result = new byte[(int) (targetEnd - targetStart + 1)]; + int bytesRead = trimmingStream.read(result, 0, result.length); + + long expectedBytesRead = targetEnd - targetStart + 1; + assertEquals(expectedBytesRead, bytesRead); + assertEquals(sourceEnd - sourceStart + 1, input.getPos()); + + // Try to read more bytes, should return -1 (end of stream) + int additionalBytesRead = trimmingStream.read(result, 0, 50); + assertEquals(-1, additionalBytesRead); + assertEquals(sourceEnd - sourceStart + 1, input.getPos()); + } + + public void testSingleByteReadInRange() throws IOException { + byte[] data = generateRandomData(100); + ReadCountInputStreamTest input = new ReadCountInputStreamTest(data); + + long sourceStart = generateRandomValue(0, 80); + long sourceEnd = generateRandomValue(sourceStart, 99); + long targetStart = generateRandomValue(sourceStart, sourceEnd); + long targetEnd = generateRandomValue(targetStart, sourceEnd); + + TrimmingStream trimmingStream = new TrimmingStream(sourceStart, sourceEnd, targetStart, targetEnd, input); + + int bytesRead = 0; + int value; + while ((value = trimmingStream.read()) != -1) { + bytesRead++; + } + + long expectedBytesRead = targetEnd - targetStart + 1; + assertEquals(expectedBytesRead, bytesRead); + assertEquals(sourceEnd - sourceStart + 1, input.getPos()); + } + + public void testInvalidInputs() { + assertThrows(IllegalArgumentException.class, () -> new TrimmingStream(-10, 60, 20, 40, new ByteArrayInputStream(new byte[100]))); + assertThrows(IllegalArgumentException.class, () -> new TrimmingStream(10, 60, 40, 20, new ByteArrayInputStream(new byte[100]))); + } + + public void testSourceSameAsTarget() throws IOException { + byte[] data = generateRandomData(100); + ReadCountInputStreamTest input = new ReadCountInputStreamTest(data); + + long sourceStart = generateRandomValue(0, 80); + long sourceEnd = generateRandomValue(sourceStart, 99); + TrimmingStream trimmingStream = new TrimmingStream(sourceStart, sourceEnd, sourceStart, sourceEnd, input); + + byte[] result = new byte[(int) (sourceEnd - sourceStart + 1)]; + int bytesRead = trimmingStream.read(result, 0, result.length); + + assertEquals(sourceEnd - sourceStart + 1, bytesRead); + assertEquals(sourceEnd - sourceStart + 1, input.getPos()); + } + + private byte[] generateRandomData(int length) { + byte[] data = new byte[length]; + for (int i = 0; i < length; i++) { + data[i] = (byte) (Math.random() * 256 - 128); + } + return data; + } + + private long generateRandomValue(long min, long max) { + return min + (long) (Math.random() * (max - min + 1)); + } +} diff --git a/modules/crypto/src/test/java/org/opensearch/encryption/frame/CipherHandlerTests.java b/modules/crypto/src/test/java/org/opensearch/encryption/frame/CipherHandlerTests.java new file mode 100644 index 0000000000000..43a0331ebbb5a --- /dev/null +++ b/modules/crypto/src/test/java/org/opensearch/encryption/frame/CipherHandlerTests.java @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.encryption.frame; + +import org.opensearch.test.OpenSearchTestCase; +import org.junit.Assert; + +import java.nio.charset.StandardCharsets; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; + +public class CipherHandlerTests extends OpenSearchTestCase { + + public void testInvalidNonce() { + CipherHandler cipherHandler = new CipherHandler(null, 1, CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256); + byte[] nonce = "random".getBytes(StandardCharsets.UTF_8); + byte[] content = "content".getBytes(StandardCharsets.UTF_8); + Assert.assertThrows(IllegalArgumentException.class, () -> cipherHandler.cipherData(nonce, null, content, 0, content.length)); + } + + public void testInvalidSecretKey() { + CryptoAlgorithm cryptoAlgorithm = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + CipherHandler cipherHandler = new CipherHandler(null, 1, cryptoAlgorithm); + byte[] nonce = new byte[cryptoAlgorithm.getNonceLen()]; + byte[] content = "content".getBytes(StandardCharsets.UTF_8); + Assert.assertThrows(AwsCryptoException.class, () -> cipherHandler.cipherData(nonce, null, content, 0, content.length)); + } +} diff --git a/modules/crypto/src/test/java/org/opensearch/encryption/frame/CryptoTests.java b/modules/crypto/src/test/java/org/opensearch/encryption/frame/CryptoTests.java new file mode 100644 index 0000000000000..c0277f29f527c --- /dev/null +++ b/modules/crypto/src/test/java/org/opensearch/encryption/frame/CryptoTests.java @@ -0,0 +1,489 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.encryption.frame; + +import org.opensearch.common.blobstore.transfer.stream.OffsetRangeFileInputStream; +import org.opensearch.common.crypto.DecryptedRangedStreamProvider; +import org.opensearch.common.crypto.EncryptedHeaderContentSupplier; +import org.opensearch.common.io.InputStreamContainer; +import org.opensearch.encryption.MockKeyProvider; +import org.opensearch.test.OpenSearchTestCase; +import org.junit.Assert; +import org.junit.Before; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.ParsedCiphertext; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import org.mockito.Mockito; + +public class CryptoTests extends OpenSearchTestCase { + + private static FrameCryptoHandler frameCryptoHandler; + + private static FrameCryptoHandler frameCryptoHandlerTrailingAlgo; + + static class CustomFrameCryptoHandlerTest extends FrameCryptoHandler { + private final int frameSize; + + CustomFrameCryptoHandlerTest(AwsCrypto awsCrypto, HashMap config, int frameSize) { + super(awsCrypto, config, () -> {}); + this.frameSize = frameSize; + } + + @Override + public int getFrameSize() { + return frameSize; + } + } + + @Before + public void setupResources() { + frameCryptoHandler = new CustomFrameCryptoHandlerTest( + createAwsCrypto(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256), + new HashMap<>(), + 100 + ); + frameCryptoHandlerTrailingAlgo = new CustomFrameCryptoHandlerTest( + createAwsCrypto(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384), + new HashMap<>(), + 100 + ); + } + + private AwsCrypto createAwsCrypto(CryptoAlgorithm cryptoAlgorithm) { + MockKeyProvider keyProvider = new MockKeyProvider(); + CachingCryptoMaterialsManager cachingMaterialsManager = CachingCryptoMaterialsManager.newBuilder() + .withMasterKeyProvider(keyProvider) + .withCache(new LocalCryptoMaterialsCache(1000)) + .withMaxAge(10, TimeUnit.MINUTES) + .build(); + + return new AwsCrypto(cachingMaterialsManager, cryptoAlgorithm); + } + + static class EncryptedStoreTest { + byte[] encryptedContent; + int encryptedLength; + long rawLength; + byte[] rawContent; + } + + private EncryptedStoreTest verifyAndGetEncryptedContent() throws IOException, URISyntaxException { + return verifyAndGetEncryptedContent(false, frameCryptoHandler); + } + + private EncryptedStoreTest verifyAndGetEncryptedContent(boolean truncateRemainderPart, FrameCryptoHandler frameCryptoHandler) + throws IOException, URISyntaxException { + + long maxLength = 50 * 1024; + long rawContentLength = truncateRemainderPart + ? maxLength / frameCryptoHandler.getFrameSize() * frameCryptoHandler.getFrameSize() + : maxLength; + byte[] rawContent = randomAlphaOfLength((int) rawContentLength).getBytes(StandardCharsets.UTF_8); + + EncryptionMetadata cryptoContext = frameCryptoHandler.initEncryptionMetadata(); + + int encLength = 0; + byte[] encryptedContent = new byte[100 * 1024]; + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(rawContent, 0, rawContent.length)) { + + InputStreamContainer stream = new InputStreamContainer(inputStream, rawContent.length, 0); + InputStreamContainer encInputStream = frameCryptoHandler.createEncryptingStream(cryptoContext, stream); + assertNotNull(encInputStream); + + int readBytes; + while ((readBytes = encInputStream.getInputStream().read(encryptedContent, encLength, 1024)) != -1) { + encLength += readBytes; + } + } + + long calculatedEncryptedLength = frameCryptoHandler.estimateEncryptedLengthOfEntireContent(cryptoContext, rawContentLength); + assertEquals(encLength, calculatedEncryptedLength); + + EncryptedStoreTest encryptedStoreTest = new EncryptedStoreTest(); + encryptedStoreTest.encryptedLength = encLength; + encryptedStoreTest.encryptedContent = encryptedContent; + encryptedStoreTest.rawLength = rawContentLength; + encryptedStoreTest.rawContent = rawContent; + return encryptedStoreTest; + } + + public void testEncryptedDecryptedLengthEstimations() { + // Testing for 100 iterations + for (int i = 0; i < 100; i++) { + // Raw content size cannot be max value as encrypted size will overflow for the same. + long n = randomLongBetween(0, Integer.MAX_VALUE / 2); + FrameCryptoHandler frameCryptoHandler = new CustomFrameCryptoHandlerTest( + createAwsCrypto(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384), + new HashMap<>(), + randomIntBetween(10, 10240) + ); + EncryptionMetadata cryptoContext = frameCryptoHandler.initEncryptionMetadata(); + long encryptedLength = frameCryptoHandler.estimateEncryptedLengthOfEntireContent(cryptoContext, n); + ParsedCiphertext parsedCiphertext = new ParsedCiphertext(cryptoContext.getCiphertextHeaderBytes()); + long decryptedLength = frameCryptoHandler.estimateDecryptedLength(parsedCiphertext, encryptedLength); + assertEquals(n, decryptedLength); + } + } + + public void testSingleStreamEncryption() throws IOException, URISyntaxException { + + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStoreTest.encryptedContent, + 0, + encryptedStoreTest.encryptedLength + ); + long decryptedRawBytes = decryptAndVerify(byteArrayInputStream, encryptedStoreTest.encryptedLength, encryptedStoreTest.rawContent); + assertEquals(encryptedStoreTest.rawLength, decryptedRawBytes); + } + + public void testSingleStreamEncryptionTrailingSignatureAlgo() throws IOException, URISyntaxException { + + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(false, frameCryptoHandlerTrailingAlgo); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStoreTest.encryptedContent, + 0, + encryptedStoreTest.encryptedLength + ); + long decryptedRawBytes = decryptAndVerify(byteArrayInputStream, encryptedStoreTest.encryptedLength, encryptedStoreTest.rawContent); + assertEquals(encryptedStoreTest.rawLength, decryptedRawBytes); + } + + public void testDecryptionOfCorruptedContent() throws IOException, URISyntaxException { + + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(); + encryptedStoreTest.encryptedContent = "Corrupted content".getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStoreTest.encryptedContent, + 0, + encryptedStoreTest.encryptedLength + ); + + Assert.assertThrows( + BadCiphertextException.class, + () -> decryptAndVerify(byteArrayInputStream, encryptedStoreTest.encryptedLength, encryptedStoreTest.rawContent) + ); + + } + + private long decryptAndVerify(InputStream encryptedStream, long encSize, byte[] rawContent) throws IOException { + long totalRawBytes = 0; + try ( + ByteArrayInputStream fis = new ByteArrayInputStream(rawContent); + InputStream decryptingStream = frameCryptoHandler.createDecryptingStream(encryptedStream) + ) { + byte[] decryptedBuffer = new byte[1024]; + byte[] actualBuffer = new byte[1024]; + int readActualBytes; + int readBytes; + while ((readBytes = decryptingStream.read(decryptedBuffer, 0, decryptedBuffer.length)) != -1) { + readActualBytes = fis.read(actualBuffer, 0, readBytes); + assertEquals(readActualBytes, readBytes); + assertArrayEquals(actualBuffer, decryptedBuffer); + totalRawBytes += readActualBytes; + } + assertEquals(rawContent.length, totalRawBytes); + } + return totalRawBytes; + } + + public void testMultiPartStreamsEncryption() throws IOException, URISyntaxException { + EncryptionMetadata encryptionMetadata = frameCryptoHandler.initEncryptionMetadata(); + + long rawContentLength = 50 * 1024; + byte[] rawContent = randomAlphaOfLength((int) rawContentLength).getBytes(StandardCharsets.UTF_8); + assertEquals(rawContentLength, rawContent.length); + + Path path = createTempFile(); + File file = path.toFile(); + Files.write(path, rawContent); + assertEquals(rawContentLength, Files.size(path)); + + byte[] encryptedContent = new byte[1024 * 100]; + int parts; + long partSize, lastPartSize; + partSize = getPartSize(rawContentLength, frameCryptoHandler.getFrameSize()); + parts = numberOfParts(rawContentLength, partSize); + lastPartSize = rawContentLength - (partSize * (parts - 1)); + + int encLength = 0; + for (int partNo = 0; partNo < parts; partNo++) { + long size = partNo == parts - 1 ? lastPartSize : partSize; + long pos = partNo * partSize; + try (InputStream inputStream = getMultiPartStreamSupplier(file).apply(size, pos)) { + InputStreamContainer rawStream = new InputStreamContainer(inputStream, size, pos); + InputStreamContainer encStream = frameCryptoHandler.createEncryptingStreamOfPart( + encryptionMetadata, + rawStream, + parts, + partNo + ); + int readBytes; + int curEncryptedBytes = 0; + while ((readBytes = encStream.getInputStream().read(encryptedContent, encLength, 1024)) != -1) { + encLength += readBytes; + curEncryptedBytes += readBytes; + } + assertEquals(encStream.getContentLength(), curEncryptedBytes); + } + } + encLength += frameCryptoHandler.getTrailingSignatureLength(encryptionMetadata); + + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encryptedContent, 0, encLength); + decryptAndVerify(byteArrayInputStream, encLength, rawContent); + + } + + private long getPartSize(long contentLength, int frameSize) { + + double optimalPartSizeDecimal = (double) contentLength / randomIntBetween(5, 10); + // round up so we don't push the upload over the maximum number of parts + long optimalPartSize = (long) Math.ceil(optimalPartSizeDecimal); + if (optimalPartSize < frameSize) { + optimalPartSize = frameSize; + } + + if (optimalPartSize >= contentLength) { + return contentLength; + } + + if (optimalPartSize % frameSize > 0) { + // When using encryption, parts must line up correctly along cipher block boundaries + optimalPartSize = optimalPartSize - (optimalPartSize % frameSize) + frameSize; + } + return optimalPartSize; + } + + private int numberOfParts(final long totalSize, final long partSize) { + if (totalSize % partSize == 0) { + return (int) (totalSize / partSize); + } + return (int) (totalSize / partSize) + 1; + } + + private BiFunction getMultiPartStreamSupplier(File localFile) { + return (size, position) -> { + OffsetRangeFileInputStream offsetRangeInputStream; + try { + offsetRangeInputStream = new OffsetRangeFileInputStream(localFile.toPath(), size, position); + } catch (IOException e) { + return null; + } + return new CheckedInputStream(offsetRangeInputStream, new CRC32()); + }; + } + + public void testBlockBasedDecryptionForEntireFile() throws Exception { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(); + validateBlockDownload(encryptedStoreTest, 0, (int) encryptedStoreTest.rawLength - 1); + } + + public void testBlockBasedDecryptionForEntireFileWithLinedUpFrameAlongFileBoundary() throws Exception { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(true, frameCryptoHandler); + assertEquals( + "This test is meant for file size exactly divisible by frame size", + 0, + (encryptedStoreTest.rawLength % frameCryptoHandler.getFrameSize()) + ); + validateBlockDownload(encryptedStoreTest, 0, (int) encryptedStoreTest.rawLength - 1); + } + + public void testCorruptedTrailingSignature() throws IOException, URISyntaxException { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(false, frameCryptoHandlerTrailingAlgo); + byte[] trailingData = "corrupted".getBytes(StandardCharsets.UTF_8); + byte[] corruptedTrailingContent = Arrays.copyOf( + encryptedStoreTest.encryptedContent, + encryptedStoreTest.encryptedContent.length + trailingData.length + ); + System.arraycopy(trailingData, 0, corruptedTrailingContent, encryptedStoreTest.encryptedContent.length, trailingData.length); + encryptedStoreTest.encryptedContent = corruptedTrailingContent; + encryptedStoreTest.encryptedLength = corruptedTrailingContent.length; + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStoreTest.encryptedContent, + 0, + encryptedStoreTest.encryptedLength + ); + BadCiphertextException ex = Assert.assertThrows( + BadCiphertextException.class, + () -> decryptAndVerify(byteArrayInputStream, encryptedStoreTest.encryptedLength, encryptedStoreTest.rawContent) + ); + Assert.assertEquals("Bad trailing signature", ex.getMessage()); + } + + public void testNoTrailingSignatureForTrailingAlgo() throws IOException, URISyntaxException { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(false, frameCryptoHandlerTrailingAlgo); + EncryptionMetadata cryptoContext = frameCryptoHandlerTrailingAlgo.initEncryptionMetadata(); + int trailingLength = frameCryptoHandler.getTrailingSignatureLength(cryptoContext); + byte[] removedTrailingContent = Arrays.copyOf( + encryptedStoreTest.encryptedContent, + encryptedStoreTest.encryptedContent.length - trailingLength + ); + encryptedStoreTest.encryptedContent = removedTrailingContent; + encryptedStoreTest.encryptedLength = removedTrailingContent.length; + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStoreTest.encryptedContent, + 0, + encryptedStoreTest.encryptedLength + ); + BadCiphertextException ex = Assert.assertThrows( + BadCiphertextException.class, + () -> decryptAndVerify(byteArrayInputStream, encryptedStoreTest.encryptedLength, encryptedStoreTest.rawContent) + ); + Assert.assertEquals("Bad trailing signature", ex.getMessage()); + } + + public void testOutputSizeEstimateWhenHandlerIsNull() { + CryptoMaterialsManager cryptoMaterialsManager = Mockito.mock(CryptoMaterialsManager.class); + DecryptionHandler decryptionHandler = DecryptionHandler.create(cryptoMaterialsManager); + int inputLen = 50; + int len = decryptionHandler.estimateOutputSize(inputLen); + assertEquals(inputLen, len); + } + + private EncryptedHeaderContentSupplier createEncryptedHeaderContentSupplier(byte[] encryptedContent) { + return (start, end) -> { + int len = (int) (end - start + 1); + byte[] bytes = new byte[len]; + System.arraycopy(encryptedContent, (int) start, bytes, (int) start, len); + return bytes; + }; + } + + public void testBlockBasedDecryptionForMiddleBlock() throws Exception { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(); + int maxBlockNum = (int) encryptedStoreTest.rawLength / frameCryptoHandler.getFrameSize(); + assert maxBlockNum > 5; + validateBlockDownload( + encryptedStoreTest, + randomIntBetween(5, maxBlockNum / 2) * frameCryptoHandler.getFrameSize(), + randomIntBetween(maxBlockNum / 2 + 1, maxBlockNum) * frameCryptoHandler.getFrameSize() - 1 + ); + } + + public void testRandomRangeDecryption() throws Exception { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(); + // Testing for 100 iterations + for (int testIteration = 0; testIteration < 100; testIteration++) { + int startPos = randomIntBetween(0, (int) encryptedStoreTest.rawLength - 1); + int endPos = randomIntBetween(startPos, (int) encryptedStoreTest.rawLength - 1); + validateBlockDownload(encryptedStoreTest, startPos, endPos); + } + } + + public void testDecryptionWithSameStartEndPos() throws Exception { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(); + int pos = randomIntBetween(0, (int) encryptedStoreTest.rawLength - 1); + for (int testIteration = 0; testIteration < frameCryptoHandler.getFrameSize(); testIteration++) { + validateBlockDownload(encryptedStoreTest, pos, pos); + } + } + + public void testBlockBasedDecryptionForLastBlock() throws Exception { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(); + int maxBlockNum = (int) encryptedStoreTest.rawLength / frameCryptoHandler.getFrameSize(); + assert maxBlockNum > 5; + validateBlockDownload( + encryptedStoreTest, + randomIntBetween(1, maxBlockNum - 1) * frameCryptoHandler.getFrameSize(), + (int) encryptedStoreTest.rawLength - 1 + ); + } + + private void validateBlockDownload(EncryptedStoreTest encryptedStoreTest, int startPos, int endPos) throws Exception { + + EncryptedHeaderContentSupplier encryptedHeaderContentSupplier = createEncryptedHeaderContentSupplier( + encryptedStoreTest.encryptedContent + ); + ParsedCiphertext cryptoContext = frameCryptoHandler.loadEncryptionMetadata(encryptedHeaderContentSupplier); + DecryptedRangedStreamProvider decryptedStreamProvider = frameCryptoHandler.createDecryptingStreamOfRange( + cryptoContext, + startPos, + endPos + ); + + long[] transformedRange = decryptedStreamProvider.getAdjustedRange(); + int encryptedBlockSize = (int) (transformedRange[1] - transformedRange[0] + 1); + byte[] encryptedBlockBytes = new byte[encryptedBlockSize]; + System.arraycopy(encryptedStoreTest.encryptedContent, (int) transformedRange[0], encryptedBlockBytes, 0, encryptedBlockSize); + ByteArrayInputStream encryptedStream = new ByteArrayInputStream(encryptedBlockBytes, 0, encryptedBlockSize); + InputStream decryptingStream = decryptedStreamProvider.getDecryptedStreamProvider().apply(encryptedStream); + + try (ByteArrayInputStream rawStream = new ByteArrayInputStream(encryptedStoreTest.rawContent, startPos, endPos - startPos + 1)) { + decryptAndVerifyBlock(decryptingStream, rawStream, startPos, endPos); + } + } + + public void testBlockBasedDecryptionForFirstBlock() throws Exception { + EncryptedStoreTest encryptedStoreTest = verifyAndGetEncryptedContent(); + // All block requests should properly line up with frames otherwise decryption will fail due to partial frames. + int blockEnd = randomIntBetween(5, (int) encryptedStoreTest.rawLength / frameCryptoHandler.getFrameSize()) * frameCryptoHandler + .getFrameSize() - 1; + validateBlockDownload(encryptedStoreTest, 0, blockEnd); + } + + private long decryptAndVerifyBlock( + InputStream decryptedStream, + ByteArrayInputStream rawStream, + int rawContentStartPos, + int rawContentEndPos + ) throws IOException { + long totalRawBytes = 0; + + byte[] decryptedBuffer = new byte[100]; + byte[] actualBuffer = new byte[100]; + + int readActualBytes; + int readBytes; + while ((readBytes = decryptedStream.read(decryptedBuffer, 0, decryptedBuffer.length)) != -1) { + readActualBytes = rawStream.read(actualBuffer, 0, Math.min(actualBuffer.length, readBytes)); + assertEquals(readActualBytes, readBytes); + assertArrayEquals(actualBuffer, decryptedBuffer); + totalRawBytes += readActualBytes; + } + assertEquals(rawContentEndPos - rawContentStartPos + 1, totalRawBytes); + return totalRawBytes; + } + + public void testEmptyContentCrypto() throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[] {}); + EncryptionMetadata cryptoContext = frameCryptoHandler.initEncryptionMetadata(); + InputStreamContainer stream = new InputStreamContainer(byteArrayInputStream, 0, 0); + InputStreamContainer encryptingStream = frameCryptoHandler.createEncryptingStream(cryptoContext, stream); + InputStream decryptingStream = frameCryptoHandler.createDecryptingStream(encryptingStream.getInputStream()); + decryptingStream.readAllBytes(); + } + + public void testEmptyContentCryptoTrailingSignatureAlgo() throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[] {}); + EncryptionMetadata cryptoContext = frameCryptoHandlerTrailingAlgo.initEncryptionMetadata(); + InputStreamContainer stream = new InputStreamContainer(byteArrayInputStream, 0, 0); + InputStreamContainer encryptingStream = frameCryptoHandlerTrailingAlgo.createEncryptingStream(cryptoContext, stream); + InputStream decryptingStream = frameCryptoHandlerTrailingAlgo.createDecryptingStream(encryptingStream.getInputStream()); + decryptingStream.readAllBytes(); + } + +} diff --git a/modules/crypto/src/test/resources/encrypted_key b/modules/crypto/src/test/resources/encrypted_key new file mode 100644 index 0000000000000..da4e503581585 --- /dev/null +++ b/modules/crypto/src/test/resources/encrypted_key @@ -0,0 +1 @@ +[1, 2, 1, 0, 120, -96, 18, 71, -6, 90, -126, -39, -16, 94, -113, -46, 71, 85, 35, -66, -117, -108, -59, 88, -81, 64, -118, -74, -102, 50, 103, 16, -76, 23, 19, 20, 67, 1, -11, 55, -3, 32, -89, -16, 1, -40, 59, 76, -2, -61, -49, -97, 34, 14, 0, 0, 0, 126, 48, 124, 6, 9, 42, -122, 72, -122, -9, 13, 1, 7, 6, -96, 111, 48, 109, 2, 1, 0, 48, 104, 6, 9, 42, -122, 72, -122, -9, 13, 1, 7, 1, 48, 30, 6, 9, 96, -122, 72, 1, 101, 3, 4, 1, 46, 48, 17, 4, 12, -63, 67, 37, -51, 85, 75, 7, -64, -78, 52, 102, 26, 2, 1, 16, -128, 59, -98, -123, 100, 125, -37, 102, -87, -71, 74, 68, 54, 56, -32, 77, 127, -86, -125, -17, 45, 75, -98, 54, -52, -15, -56, -47, -88, -12, -128, 113, -5, -18, -14, 127, 114, -9, 47, -112, -38, 39, 2, -89, 117, 64, -2, 47, -81, 52, 27, -118, 37, 79, -64, 58, -3, 10, -115, 122, 124] \ No newline at end of file diff --git a/modules/crypto/src/test/resources/raw_key b/modules/crypto/src/test/resources/raw_key new file mode 100644 index 0000000000000..3c4f8b54cbb6a --- /dev/null +++ b/modules/crypto/src/test/resources/raw_key @@ -0,0 +1 @@ +[57, 59, -48, -8, -44, 9, -78, 16, 106, -80, 66, -41, 66, 43, -88, 7, 47, -23, -16, -43, 99, 104, -8, -74, 46, -117, -111, -41, -39, -69, 5, 117] \ No newline at end of file