diff --git a/libs/common/src/main/java/org/opensearch/common/crypto/DecryptedRangedStreamProvider.java b/libs/common/src/main/java/org/opensearch/common/crypto/DecryptedRangedStreamProvider.java new file mode 100644 index 0000000000000..b0c7d66c0437a --- /dev/null +++ b/libs/common/src/main/java/org/opensearch/common/crypto/DecryptedRangedStreamProvider.java @@ -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. + */ + +package org.opensearch.common.crypto; + +import java.io.InputStream; +import java.util.function.Function; + +/** + * Contains adjusted range of partial encrypted content which needs to be used for decryption. + */ +public class DecryptedRangedStreamProvider { + + private final long[] adjustedRange; + private final Function decryptedStreamProvider; + + /** + * To construct adjusted encrypted range. + * @param adjustedRange range of partial encrypted content which needs to be used for decryption. + * @param decryptedStreamProvider stream provider for decryption and range re-adjustment. + */ + public DecryptedRangedStreamProvider(long[] adjustedRange, Function decryptedStreamProvider) { + this.adjustedRange = adjustedRange; + this.decryptedStreamProvider = decryptedStreamProvider; + } + + + /** + * Adjusted range of partial encrypted content which needs to be used for decryption. + * @return adjusted range + */ + public long[] getAdjustedRange() { + return adjustedRange; + } + + + /** + * A utility stream provider which supplies the stream responsible for decrypting the content and reading the + * desired range of decrypted content by skipping extra content which got decrypted as a result of range adjustment. + * @return stream provider for decryption and supplying the desired range of content. + */ + public Function getDecryptedStreamProvider() { + return decryptedStreamProvider; + } + + +} diff --git a/libs/common/src/main/java/org/opensearch/common/crypto/EncryptedHeaderContentSupplier.java b/libs/common/src/main/java/org/opensearch/common/crypto/EncryptedHeaderContentSupplier.java new file mode 100644 index 0000000000000..49a037f05f185 --- /dev/null +++ b/libs/common/src/main/java/org/opensearch/common/crypto/EncryptedHeaderContentSupplier.java @@ -0,0 +1,25 @@ +/* + * 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.common.crypto; + +import java.io.IOException; + +/** + * This is used in partial decryption. Header information is required for decryption of actual encrypted content. + * Implementation of this supplier only requires first few bytes of encrypted content to be supplied. + */ +public interface EncryptedHeaderContentSupplier { + + /** + * @param start Start position of the encrypted content (Generally supplied as 0 during usage) + * @param end End position of the header. + * @return Encrypted header content (May contain additional content which is later discarded) + * @throws IOException In case content fetch fails. + */ + byte[] supply(long start, long end) throws IOException; +} diff --git a/libs/common/src/main/java/org/opensearch/common/crypto/package-info.java b/libs/common/src/main/java/org/opensearch/common/crypto/package-info.java new file mode 100644 index 0000000000000..c744689ebf532 --- /dev/null +++ b/libs/common/src/main/java/org/opensearch/common/crypto/package-info.java @@ -0,0 +1,10 @@ +/* + * 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. + */ + +/** Common crypto utilities used across opensearch. */ +package org.opensearch.common.crypto; diff --git a/libs/encryption-sdk/build.gradle b/libs/encryption-sdk/build.gradle new file mode 100644 index 0000000000000..456e1ec1788df --- /dev/null +++ b/libs/encryption-sdk/build.gradle @@ -0,0 +1,43 @@ +/* + * 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 + +dependencies { + api project(':libs:opensearch-common') + implementation "com.amazonaws:aws-encryption-sdk-java:2.4.0" + implementation "org.bouncycastle:bcprov-jdk15to18:${versions.bouncycastle}" + implementation 'org.apache.commons:commons-lang3:3.12.0' + + testImplementation "junit:junit:${versions.junit}" + testImplementation "org.hamcrest:hamcrest:${versions.hamcrest}" + + testImplementation(project(":test:framework")) { + exclude group: 'org.opensearch', module: 'opensearch-encryption-sdk' + } +} + +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/libs/encryption-sdk/licenses/aws-encryption-sdk-java-2.4.0.jar.sha1 b/libs/encryption-sdk/licenses/aws-encryption-sdk-java-2.4.0.jar.sha1 new file mode 100644 index 0000000000000..504b4a423a975 --- /dev/null +++ b/libs/encryption-sdk/licenses/aws-encryption-sdk-java-2.4.0.jar.sha1 @@ -0,0 +1 @@ +98943eda1dc05bb01f4f5405e115b08dc541afbf \ No newline at end of file diff --git a/libs/encryption-sdk/licenses/aws-encryption-sdk-java-LICENSE.txt b/libs/encryption-sdk/licenses/aws-encryption-sdk-java-LICENSE.txt new file mode 100644 index 0000000000000..8dada3edaf50d --- /dev/null +++ b/libs/encryption-sdk/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/libs/encryption-sdk/licenses/aws-encryption-sdk-java-NOTICE.txt b/libs/encryption-sdk/licenses/aws-encryption-sdk-java-NOTICE.txt new file mode 100644 index 0000000000000..e32695955374a --- /dev/null +++ b/libs/encryption-sdk/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/libs/encryption-sdk/licenses/bcprov-jdk15to18-1.75.jar.sha1 b/libs/encryption-sdk/licenses/bcprov-jdk15to18-1.75.jar.sha1 new file mode 100644 index 0000000000000..9911bb75f9209 --- /dev/null +++ b/libs/encryption-sdk/licenses/bcprov-jdk15to18-1.75.jar.sha1 @@ -0,0 +1 @@ +df22e1b6a9f6b218913f5b68dd16641344397fe0 \ No newline at end of file diff --git a/libs/encryption-sdk/licenses/bcprov-jdk15to18-LICENSE.txt b/libs/encryption-sdk/licenses/bcprov-jdk15to18-LICENSE.txt new file mode 100644 index 0000000000000..9f27bafe96885 --- /dev/null +++ b/libs/encryption-sdk/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/libs/encryption-sdk/licenses/bcprov-jdk15to18-NOTICE.txt b/libs/encryption-sdk/licenses/bcprov-jdk15to18-NOTICE.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/libs/encryption-sdk/licenses/commons-lang3-3.12.0.jar.sha1 b/libs/encryption-sdk/licenses/commons-lang3-3.12.0.jar.sha1 new file mode 100644 index 0000000000000..9273d8c01aaba --- /dev/null +++ b/libs/encryption-sdk/licenses/commons-lang3-3.12.0.jar.sha1 @@ -0,0 +1 @@ +c6842c86792ff03b9f1d1fe2aab8dc23aa6c6f0e \ No newline at end of file diff --git a/libs/encryption-sdk/licenses/commons-lang3-LICENSE.txt b/libs/encryption-sdk/licenses/commons-lang3-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/libs/encryption-sdk/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/libs/encryption-sdk/licenses/commons-lang3-NOTICE.txt b/libs/encryption-sdk/licenses/commons-lang3-NOTICE.txt new file mode 100644 index 0000000000000..13a3140897472 --- /dev/null +++ b/libs/encryption-sdk/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/libs/encryption-sdk/src/forbidden/crypto-signatures.txt b/libs/encryption-sdk/src/forbidden/crypto-signatures.txt new file mode 100644 index 0000000000000..3699186679924 --- /dev/null +++ b/libs/encryption-sdk/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/libs/encryption-sdk/src/forbidden/crypto-test-signatures.txt b/libs/encryption-sdk/src/forbidden/crypto-test-signatures.txt new file mode 100644 index 0000000000000..3699186679924 --- /dev/null +++ b/libs/encryption-sdk/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/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/FrameCryptoProvider.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/FrameCryptoProvider.java new file mode 100644 index 0000000000000..278999001ac6e --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/FrameCryptoProvider.java @@ -0,0 +1,272 @@ +/* + * 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 com.amazonaws.encryptionsdk.ParsedCiphertext; +import org.opensearch.common.crypto.DecryptedRangedStreamProvider; +import org.opensearch.common.crypto.EncryptedHeaderContentSupplier; +import org.opensearch.encryption.frame.core.AwsCrypto; +import org.opensearch.encryption.frame.core.EncryptionMetadata; +import org.opensearch.common.io.InputStreamContainer; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +public class FrameCryptoProvider { + private final AwsCrypto awsCrypto; + private final Map encryptionContext; + + // package private for tests + private final int FRAME_SIZE = 8 * 1024; + + public FrameCryptoProvider(AwsCrypto awsCrypto, Map encryptionContext) { + this.awsCrypto = awsCrypto; + this.encryptionContext = encryptionContext; + } + + 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 Object 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 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 adjustEncryptedStreamSize(Object cryptoContextObj, long streamSize) { + EncryptionMetadata encryptionMetadata = validateEncryptionMetadata(cryptoContextObj); + return (streamSize - (streamSize % encryptionMetadata.getFrameSize())) + encryptionMetadata.getFrameSize(); + } + + /** + * 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 estimateEncryptedLength(Object cryptoMetadataObj, long contentLength) { + EncryptionMetadata encryptionMetadata = validateEncryptionMetadata(cryptoMetadataObj); + return encryptionMetadata.getCiphertextHeaderBytes().length + awsCrypto.estimateOutputSizeWithFooter( + encryptionMetadata.getFrameSize(), + encryptionMetadata.getNonceLen(), + encryptionMetadata.getCryptoAlgo().getTagLen(), + contentLength, + encryptionMetadata.getCryptoAlgo() + ); + } + + /** + * Estimate length of the decrypted stream. + * + * @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) { + if (!(cryptoMetadataObj instanceof ParsedCiphertext)) { + throw new IllegalArgumentException("Unknown crypto metadata object received for adjusting range for decryption"); + } + ParsedCiphertext parsedCiphertext = (ParsedCiphertext) cryptoMetadataObj; + return awsCrypto.estimateDecryptedSize( + parsedCiphertext.getFrameLength(), + parsedCiphertext.getNonceLength(), + parsedCiphertext.getCryptoAlgoId().getTagLen(), + contentLength - parsedCiphertext.getOffset(), + parsedCiphertext.getCryptoAlgoId() + ); + } + + /** + * Wraps a raw InputStream with encrypting stream + * @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) { + EncryptionMetadata encryptionMetadata = validateEncryptionMetadata(cryptoContextObj); + return createEncryptingStreamOfPart(encryptionMetadata, stream, 1, 0); + } + + private EncryptionMetadata validateEncryptionMetadata(Object cryptoContext) { + if (!(cryptoContext instanceof EncryptionMetadata)) { + throw new IllegalArgumentException("Unknown crypto metadata object received"); + } + return (EncryptionMetadata) cryptoContext; + } + + /** + * 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 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 + ) { + EncryptionMetadata encryptionMetadata = parseEncryptionMetadata(cryptoContextObj); + + boolean includeHeader = streamIdx == 0; + boolean includeFooter = streamIdx == (totalStreams - 1); + int frameStartNumber = (int) (stream.getOffset() / getFrameSize()) + 1; + + return awsCrypto.createEncryptingStream( + stream, + streamIdx, + totalStreams, + frameStartNumber, + includeHeader, + includeFooter, + encryptionMetadata + ); + } + + private EncryptionMetadata parseEncryptionMetadata(Object cryptoContextObj) { + if (!(cryptoContextObj instanceof EncryptionMetadata)) { + throw new IllegalArgumentException("Unknown crypto metadata object received"); + } + return (EncryptionMetadata) cryptoContextObj; + } + + /** + * + * @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 { + 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 cryptoContextObj Context object needed to calculate trailing length. + * @return Trailing signature length + */ + public int getTrailingSignatureLength(Object cryptoContextObj) { + EncryptionMetadata encryptionMetadata = parseEncryptionMetadata(cryptoContextObj); + return awsCrypto.getTrailingSignatureSize(encryptionMetadata.getCryptoAlgo()); + } + + private InputStream createBlockDecryptionStream( + Object cryptoContext, + InputStream inputStream, + long startPosOfRawContent, + long endPosOfRawContent, + long[] encryptedRange + ) { + ParsedCiphertext parsedCiphertext = (ParsedCiphertext) cryptoContext; + 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 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 + ) { + if (!(cryptoContext instanceof ParsedCiphertext)) { + throw new IllegalArgumentException("Unknown crypto metadata object received for adjusting range for decryption"); + } + + ParsedCiphertext encryptionMetadata = (ParsedCiphertext) cryptoContext; + 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( + cryptoContext, + 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 }; + } + +} diff --git a/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/TrimmingStream.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/TrimmingStream.java new file mode 100644 index 0000000000000..9df354dc5619c --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/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.frame; + +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/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/AwsCrypto.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/AwsCrypto.java new file mode 100644 index 0000000000000..6ac4a79b6cf74 --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/AwsCrypto.java @@ -0,0 +1,157 @@ +/* + * 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.core; + +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.internal.SignaturePolicy; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; +import org.opensearch.common.io.InputStreamContainer; + +import java.io.InputStream; +import java.util.Map; + +public class AwsCrypto { + private final CryptoMaterialsManager materialsManager; + private static final CommitmentPolicy DEFAULT_COMMITMENT_POLICY = CommitmentPolicy.RequireEncryptRequireDecrypt; + 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(DEFAULT_COMMITMENT_POLICY); + + return new EncryptionMetadata( + frameSize, + materialsManager.getMaterialsForEncrypt(requestBuilder.build()), + DEFAULT_COMMITMENT_POLICY + ); + } + + public InputStreamContainer createEncryptingStream( + final InputStreamContainer stream, + int streamIdx, + int totalStreams, + int frameNumber, + boolean includeHeader, + boolean includeFooter, + EncryptionMetadata encryptionMetadata + ) { + + boolean isLastStream = streamIdx == totalStreams - 1 && includeFooter; + boolean firstOperation = streamIdx == 0 && includeHeader; + 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, + DEFAULT_COMMITMENT_POLICY, + SignaturePolicy.AllowEncryptAllowDecrypt, + 1 + ); + return new CryptoInputStream<>(inputStream, cryptoHandler, true); + } + + public CryptoInputStream createDecryptingStream( + final InputStream inputStream, + final long size, + final ParsedCiphertext parsedCiphertext, + final int frameStartNum, + boolean lastPart + ) { + + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create( + materialsManager, + parsedCiphertext, + DEFAULT_COMMITMENT_POLICY, + SignaturePolicy.AllowEncryptAllowDecrypt, + 1, + frameStartNum + ); + CryptoInputStream cryptoInputStream = new CryptoInputStream<>(inputStream, cryptoHandler, lastPart); + cryptoInputStream.setMaxInputLength(size); + return cryptoInputStream; + } + +} diff --git a/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/CipherHandler.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/CipherHandler.java new file mode 100644 index 0000000000000..f723260110795 --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/CipherHandler.java @@ -0,0 +1,94 @@ +/* + * 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.core; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import java.security.GeneralSecurityException; +import java.security.spec.AlgorithmParameterSpec; + +/** + * 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: " + 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/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/Constants.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/Constants.java new file mode 100644 index 0000000000000..fc05d9e2a4572 --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/Constants.java @@ -0,0 +1,39 @@ +/* + * 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.core; + +public final class Constants { + /** + * Default length of the message identifier used to uniquely identify every ciphertext created by + * this library. + * + */ + @Deprecated + public static final int MESSAGE_ID_LEN = 16; + + private Constants() { + // Prevent instantiation + } + + /** Marker for identifying the final frame. */ + public static final int ENDFRAME_SEQUENCE_NUMBER = ~0; // is 0xFFFFFFFF + + /** + * The identifier for non-final frames in the framing content type. This value is used as part of + * the additional authenticated data (AAD) when encryption of content in a frame. + */ + public static final String FRAME_STRING_ID = "AWSKMSEncryptionClient Frame"; + + /** + * The identifier for the final frame in the framing content type. This value is used as part of + * the additional authenticated data (AAD) when encryption of content in a frame. + */ + public static final String FINAL_FRAME_STRING_ID = "AWSKMSEncryptionClient Final Frame"; + + public static final long MAX_FRAME_NUMBER = (1L << 32) - 1; +} diff --git a/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/CryptoInputStream.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/CryptoInputStream.java new file mode 100644 index 0000000000000..7c41b5b8ab6cc --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/CryptoInputStream.java @@ -0,0 +1,228 @@ +/* + * 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.core; + +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; + +import java.io.IOException; +import java.io.InputStream; + +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. + * @param isLastPart Whether the provided InputStream is the last stream of the content. + */ + public CryptoInputStream(final InputStream inputStream, final MessageCryptoHandler cryptoHandler, final 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); + + return processBytes(readLen, inputStreamBytes); + } + + private int processBytes(int readLen, byte[] inputStreamBytes) throws BadCiphertextException { + + outStart_ = 0; + + int processedLen = -1; + if (readLen < 0 && isLastPart_) { + // Mark end of stream until doFinal returns something. + processedLen = -1; + + 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. + * + *

If this method is called multiple times, the smallest bound will be used. + * + * @param size Maximum input size. + */ + public void setMaxInputLength(long size) { + cryptoHandler_.setMaxInputLength(size); + } +} diff --git a/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/DecryptionHandler.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/DecryptionHandler.java new file mode 100644 index 0000000000000..2079b28402106 --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/DecryptionHandler.java @@ -0,0 +1,611 @@ +/* + * 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.core; + +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.DataKey; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.ParsedCiphertext; +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.internal.SignaturePolicy; +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; + +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; + +/** + * This class implementation is referenced from AWS Encryption SDK + * https://tiny.amazon.com/lfkvesll/codeamazpackblobheadamaz4 + * + * 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 {@link FrameDecryptionHandler} based + * on the content type parsed in the ciphertext headers. + */ +@SuppressWarnings("unchecked") +public class DecryptionHandler> implements MessageCryptoHandler { + private final CryptoMaterialsManager materialsManager_; + private final CommitmentPolicy commitmentPolicy_; + /** + * The maximum number of encrypted data keys to parse, if positive. If zero, do not limit EDKs. + */ + private final int maxEncryptedDataKeys_; + + private final SignaturePolicy signaturePolicy_; + + 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, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys + ) { + Utils.assertNonNull(materialsManager, "materialsManager"); + Utils.assertNonNull(commitmentPolicy, "commitmentPolicy"); + Utils.assertNonNull(signaturePolicy, "signaturePolicy"); + + this.materialsManager_ = materialsManager; + this.commitmentPolicy_ = commitmentPolicy; + this.maxEncryptedDataKeys_ = maxEncryptedDataKeys; + this.signaturePolicy_ = signaturePolicy; + ciphertextHeaders_ = new CiphertextHeaders(); + ciphertextFooters_ = new CiphertextFooters(); + } + + private DecryptionHandler( + final CryptoMaterialsManager materialsManager, + final CiphertextHeaders headers, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys, + final int frameStartNum + ) throws AwsCryptoException { + Utils.assertNonNull(materialsManager, "materialsManager"); + Utils.assertNonNull(commitmentPolicy, "commitmentPolicy"); + Utils.assertNonNull(signaturePolicy, "signaturePolicy"); + + materialsManager_ = materialsManager; + ciphertextHeaders_ = headers; + commitmentPolicy_ = commitmentPolicy; + signaturePolicy_ = signaturePolicy; + maxEncryptedDataKeys_ = maxEncryptedDataKeys; + ciphertextFooters_ = new CiphertextFooters(); + readHeaderFields(headers, frameStartNum); + updateTrailingSignature(headers); + } + + /** + * 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. + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys The maximum number of encrypted data keys to unwrap during + * decryption; zero indicates no maximum + * @throws AwsCryptoException if the master key is null. + * @return instance of {@link DecryptionHandler} + */ + public static DecryptionHandler create( + final CryptoMaterialsManager materialsManager, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys + ) throws AwsCryptoException { + return new DecryptionHandler<>(materialsManager, commitmentPolicy, signaturePolicy, maxEncryptedDataKeys); + } + + /** + * 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)} + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys The maximum number of encrypted data keys to unwrap during + * decryption; zero indicates no maximum + * @throws AwsCryptoException if the master key is null. + * @deprecated This version may have to recalculate the number of bytes already parsed, which adds + * a performance penalty. Use {@link #create(CryptoMaterialsManager, ParsedCiphertext, + * CommitmentPolicy, SignaturePolicy, int, int)} instead, which makes the parsed byte count + * directly available instead. + * @param frameStartNum Number from which assignment has to start for new frames + * @return instance of {@link DecryptionHandler} + */ + @Deprecated + public static DecryptionHandler create( + final CryptoMaterialsManager materialsManager, + final CiphertextHeaders headers, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys, + final int frameStartNum + ) throws AwsCryptoException { + return new DecryptionHandler<>(materialsManager, headers, commitmentPolicy, signaturePolicy, maxEncryptedDataKeys, frameStartNum); + } + + /** + * 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)} + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys The maximum number of encrypted data keys to unwrap during + * 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 ParsedCiphertext headers, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys, + final int frameStartNum + ) throws AwsCryptoException { + return new DecryptionHandler<>(materialsManager, headers, commitmentPolicy, signaturePolicy, maxEncryptedDataKeys, 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 { + + // We should arguably check if we are already complete_ here as other handlers + // like FrameDecryptionHandler and BlockDecryptionHandler do. + // However, adding that now could potentially break customers who have extra trailing + // bytes in their decryption streams. + // The handlers are also inconsistent in general with this check. Even those that + // do raise an exception here if already complete will not complain if + // a single call to processBytes() completes the message and provides extra trailing bytes: + // in that case they will just indicate that they didn't process the extra bytes instead. + + 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_) { + totalParsedBytes += ciphertextHeaders_.deserialize(bytesToParse, 0, maxEncryptedDataKeys_); + // 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()) { + // reset unparsed bytes as parsing of the ciphertext footer is + // complete. + // This isn't strictly necessary since processing any further data + // should be an error. + unparsedBytes_ = new byte[0]; + + 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 { + // If there aren't enough bytes to parse the ciphertext + // footer, we don't have any more bytes to continue parsing. + // But first copy the leftover bytes to unparsed bytes. + unparsedBytes_ = Arrays.copyOfRange(bytesToParse, totalParsedBytes, bytesToParse.length); + return new ProcessingSummary(actualOutLen, len); + } + } 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 { + // This is an unfortunate special case we have to support for backwards-compatibility. + if (ciphertextBytesSupplied_ == 0) { + return 0; + } + + // 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) { + throw new BadCiphertextException("Unable to process entire ciphertext."); + } else { + int result = contentCryptoHandler_.doFinal(out, outOff); + + if (!complete_) { + 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 (inLen > 0) ? inLen : 0; + } + } + + @Override + public int estimatePartialOutputSize(int inLen) { + if (contentCryptoHandler_ != null) { + return contentCryptoHandler_.estimatePartialOutputSize(inLen); + } else { + return (inLen > 0) ? 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(); + + if (!commitmentPolicy_.algorithmAllowedForDecrypt(cryptoAlgo_)) { + throw new AwsCryptoException( + "Configuration conflict. " + + "Cannot decrypt message with ID " + + messageId + + " due to CommitmentPolicy " + + commitmentPolicy_ + + " requiring only committed messages. Algorithm ID was " + + cryptoAlgo_ + + ". See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/troubleshooting-migration.html" + ); + } + + if (maxEncryptedDataKeys_ > 0 && ciphertextHeaders_.getEncryptedKeyBlobCount() > maxEncryptedDataKeys_) { + throw new AwsCryptoException("Ciphertext encrypted data keys exceed maxEncryptedDataKeys"); + } + + if (!signaturePolicy_.algorithmAllowedForDecrypt(cryptoAlgo_)) { + throw new AwsCryptoException( + "Configuration conflict. " + + "Cannot decrypt message with ID " + + messageId + + " because AwsCrypto.createUnsignedMessageDecryptingStream() " + + " accepts only unsigned messages. Algorithm ID was " + + cryptoAlgo_ + + "." + ); + } + + 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); + + switch (contentType) { + case FRAME: + contentCryptoHandler_ = new FrameDecryptionHandler( + decryptionKey_, + (byte) nonceLen, + cryptoAlgo_, + messageId, + frameLen, + frameStartNum + ); + break; + default: + // should never get here because an invalid content type is + // detected when parsing. + break; + } + + 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/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/EncryptionHandler.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/EncryptionHandler.java new file mode 100644 index 0000000000000..c28db79b8bbe5 --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/EncryptionHandler.java @@ -0,0 +1,326 @@ +/* + * 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.core; + +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.KeyBlob; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; + +import javax.crypto.SecretKey; +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; + +/** + * 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 + * {@link FrameEncryptionHandler}. + */ +public class EncryptionHandler implements MessageCryptoHandler { + + 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 CryptoHandler contentCryptoHandler_; + + private boolean firstOperation_; + private boolean complete_ = false; + + private long plaintextBytes_ = 0; + private long plaintextByteLimit_ = -1; + + private final PrivateKey trailingSignaturePrivateKey; + private final MessageDigest trailingDigest; + private final Signature trailingSig; + + /** + * 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 { + encryptionContext_ = encryptionMetadata.getEncryptionContext(); + cryptoAlgo_ = encryptionMetadata.getCryptoAlgo(); + masterKeys_ = encryptionMetadata.getMasterKeys(); + keyBlobs_ = encryptionMetadata.getKeyBlobs(); + encryptionKey_ = encryptionMetadata.getEncryptionKey(); + version_ = encryptionMetadata.getVersion(); + type_ = encryptionMetadata.getType(); + nonceLen_ = encryptionMetadata.getNonceLen(); + ciphertextHeaders_ = encryptionMetadata.getCiphertextHeaders(); + ciphertextHeaderBytes_ = encryptionMetadata.getCiphertextHeaderBytes(); + firstOperation_ = isFirstStream; + byte[] messageId = encryptionMetadata.getMessageId(); + trailingSignaturePrivateKey = encryptionMetadata.getTrailingSignaturePrivateKey(); + trailingDigest = encryptionMetadata.getTrailingDigest(); + trailingSig = encryptionMetadata.getTrailingSig(); + contentCryptoHandler_ = new FrameEncryptionHandler( + encryptionKey_, + nonceLen_, + cryptoAlgo_, + messageId, + encryptionMetadata.getFrameSize(), + frameStartNumber + ); + } + + /** + * 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. else, pass off the input data to underlying content cryptohandler. + *
+ * + * @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_) { + 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 (this.trailingDigest != null) { + this.trailingDigest.update(input, offset, len); + } else if (this.trailingSig != null) { + try { + this.trailingSig.update(input, offset, len); + } catch (SignatureException var5) { + throw new AwsCryptoException(var5); + } + } + + } + + @Override + public boolean isComplete() { + return complete_; + } +} diff --git a/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/EncryptionMetadata.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/EncryptionMetadata.java new file mode 100644 index 0000000000000..370e8a5914a9a --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/EncryptionMetadata.java @@ -0,0 +1,236 @@ +/* + * 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.core; + +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.CommittedKey; +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; + +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; + +@SuppressWarnings("unchecked") +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; + + public EncryptionMetadata(int frameSize, EncryptionMaterials result, CommitmentPolicy commitmentPolicy) throws AwsCryptoException { + Utils.assertNonNull(result, "result"); + Utils.assertNonNull(commitmentPolicy, "commitmentPolicy"); + + this.encryptionContext_ = result.getEncryptionContext(); + if (!commitmentPolicy.algorithmAllowedForEncrypt(result.getAlgorithm())) { + if (commitmentPolicy == CommitmentPolicy.ForbidEncryptAllowDecrypt) { + throw new AwsCryptoException( + "Configuration conflict. Cannot encrypt due to CommitmentPolicy " + + commitmentPolicy + + " requiring only non-committed messages. Algorithm ID was " + + result.getAlgorithm() + ); + } else { + throw new AwsCryptoException( + "Configuration conflict. Cannot encrypt due to CommitmentPolicy " + + commitmentPolicy + + " requiring only committed messages. Algorithm ID was " + + result.getAlgorithm() + ); + } + } + + 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, Utils.getSecureRandom()); + } catch (final GeneralSecurityException ex) { + throw new AwsCryptoException(ex); + } + } else { + trailingDigest = null; + trailingSig = null; + } + + // set default values + version = cryptoAlgo.getMessageFormatVersion(); + type = CIPHERTEXT_TYPE; + nonceLen = cryptoAlgo.getNonceLen(); + + ContentType contentType; + if (frameSize > 0) { + contentType = ContentType.FRAME; + } else if (frameSize == 0) { + contentType = ContentType.SINGLEBLOCK; + } else { + throw Utils.cannotBeNegative("Frame size"); + } + + // Construct the headers + // Included here rather than as a sub-routine so we can set final variables. + // This way we can avoid calculating the keys more times than we need. + final byte[] encryptionContextBytes = EncryptionContextSerializer.serialize(encryptionContext_); + final CiphertextHeaders unsignedHeaders = new CiphertextHeaders( + type, + cryptoAlgo, + encryptionContextBytes, + keyBlobs, + contentType, + frameSize + ); + // We use a deterministic IV of zero for the header authentication. + unsignedHeaders.setHeaderNonce(new byte[nonceLen]); + + // If using a committing crypto algorithm, we also need to calculate the commitment value along + // with the key derivation + if (cryptoAlgo.isCommitting()) { + final CommittedKey committedKey = CommittedKey.generate( + cryptoAlgo, + result.getCleartextDataKey(), + unsignedHeaders.getMessageId() + ); + unsignedHeaders.setSuiteData(committedKey.getCommitment()); + encryptionKey = committedKey.getKey(); + } else { + 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; + } + + 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/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/FrameDecryptionHandler.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/FrameDecryptionHandler.java new file mode 100644 index 0000000000000..9645878c9f0dc --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/FrameDecryptionHandler.java @@ -0,0 +1,296 @@ +/* + * 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.core; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.CryptoHandler; +import com.amazonaws.encryptionsdk.internal.ProcessingSummary; +import com.amazonaws.encryptionsdk.model.CipherFrameHeaders; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.util.Arrays; + +/** + * This class implementation has been referenced from AWS encryption SDK. + * https://tiny.amazon.com/1j988pk3f/codeamazpackblobheadamaz4 + * + * 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. parse the ciphertext until encrypted content in a frame is available + *
  3. decrypt the encrypted content + *
  4. return decrypted bytes as output + *
+ * + * @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 out and processed + * @throws BadCiphertextException if frame number is invalid/out-of-order or if the bytes do not + * decrypt correctly. + * @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 BadCiphertextException, 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_ == 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 contentSize = frames * frameSize; + + // Calculate the size of the last frame + long lastFrameSize = 0; + + // 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 + lastFrameSize = encryptedSize - contentSize - headerOverhead - lastFrameHeaderOverhead; + + return contentSize + 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/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/FrameEncryptionHandler.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/FrameEncryptionHandler.java new file mode 100644 index 0000000000000..aceb0a737a37f --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/FrameEncryptionHandler.java @@ -0,0 +1,344 @@ +/* + * 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.core; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.CryptoHandler; +import com.amazonaws.encryptionsdk.internal.ProcessingSummary; +import com.amazonaws.encryptionsdk.model.CipherFrameHeaders; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * 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. + */ +public 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_ = 1; + 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. + * + * @param encryptionKey Encryption key used to encrypt file content. Appended to file header. + * @param nonceLen random number appended with every frame header. + * @param cryptoAlgo Crypto algorithm used to perform encryption + * @param messageId Message Id from the crypto header + * @param frameSize Size of a frame. + * @param frameStartNumber Number from which assignment has to start for new 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. call processBytes() of the underlying cipher to do corresponding cryptographic encryption + * of plaintext + *
  3. check if current frame is fully filled using the processed bytes, write current frame to + * the output being returned. + *
+ * + * @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_ > 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_ == 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_, Constants.FINAL_FRAME_STRING_ID, (int) frameNumber_, len); + } else { + contentAad = Utils.generateContentAad(messageId_, 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/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/Utils.java b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/Utils.java new file mode 100644 index 0000000000000..83f1810919ee9 --- /dev/null +++ b/libs/encryption-sdk/src/main/java/org/opensearch/encryption/frame/core/Utils.java @@ -0,0 +1,92 @@ +/* + * 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.core; + +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/libs/encryption-sdk/src/test/java/org/opensearch/encryption/MockKeyProvider.java b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/MockKeyProvider.java new file mode 100644 index 0000000000000..23ce7a16363b5 --- /dev/null +++ b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/MockKeyProvider.java @@ -0,0 +1,108 @@ +/* + * 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 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; + +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; + +@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/libs/encryption-sdk/src/test/java/org/opensearch/encryption/OffsetRangeFileInputStream.java b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/OffsetRangeFileInputStream.java new file mode 100644 index 0000000000000..5bc4cede601e3 --- /dev/null +++ b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/OffsetRangeFileInputStream.java @@ -0,0 +1,105 @@ +/* + * 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.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +public class OffsetRangeFileInputStream extends InputStream implements Closeable { + private final InputStream inputStream; + private final FileChannel fileChannel; + private final long actualSizeToRead; + private final long limit; + private long counter = 0L; + private long markPointer; + private long markCounter; + + public OffsetRangeFileInputStream(Path path, long size, long position) throws IOException { + this.fileChannel = FileChannel.open(path, StandardOpenOption.READ); + this.fileChannel.position(position); + this.inputStream = Channels.newInputStream(this.fileChannel); + long totalLength = this.fileChannel.size(); + this.counter = 0L; + this.limit = size; + if (totalLength - position > this.limit) { + this.actualSizeToRead = this.limit; + } else { + this.actualSizeToRead = totalLength - position; + } + + } + + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off >= 0 && len >= 0 && len <= b.length - off) { + if (this.fileChannel.position() >= this.fileChannel.size()) { + return -1; + } else { + if (this.fileChannel.position() + (long) len > this.fileChannel.size()) { + len = (int) (this.fileChannel.size() - this.fileChannel.position()); + } + + if (this.counter + (long) len > this.limit) { + len = (int) (this.limit - this.counter); + } + + if (len <= 0) { + return -1; + } else { + this.inputStream.read(b, off, len); + this.counter += (long) len; + return len; + } + } + } else { + throw new IndexOutOfBoundsException(); + } + } + + public int read() throws IOException { + if (this.counter++ >= this.limit) { + return -1; + } else { + return this.fileChannel.position() < this.fileChannel.size() ? this.inputStream.read() & 255 : -1; + } + } + + public boolean markSupported() { + return true; + } + + public synchronized void mark(int readlimit) { + try { + this.markPointer = this.fileChannel.position(); + } catch (IOException var3) { + throw new RuntimeException(var3); + } + + this.markCounter = this.counter; + } + + public synchronized void reset() throws IOException { + this.fileChannel.position(this.markPointer); + this.counter = this.markCounter; + } + + public long getFilePointer() throws IOException { + return this.fileChannel.position(); + } + + public void close() throws IOException { + this.inputStream.close(); + } +} diff --git a/libs/encryption-sdk/src/test/java/org/opensearch/encryption/core/CipherHandlerTests.java b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/core/CipherHandlerTests.java new file mode 100644 index 0000000000000..d4026a4560942 --- /dev/null +++ b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/core/CipherHandlerTests.java @@ -0,0 +1,37 @@ +/* + * 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.core; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import org.junit.Assert; +import org.junit.Test; +import org.opensearch.encryption.frame.core.CipherHandler; + +import java.nio.charset.StandardCharsets; + +public class CipherHandlerTests { + + @Test + public void testInvalidNonce() { + CipherHandler cipherHandler = new CipherHandler(null, 1, CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY); + 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)); + } + + @Test + public void testInvalidSecretKey() { + CryptoAlgorithm cryptoAlgorithm = CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; + 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/libs/encryption-sdk/src/test/java/org/opensearch/encryption/core/EncryptionMetadataTests.java b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/core/EncryptionMetadataTests.java new file mode 100644 index 0000000000000..a38af61549212 --- /dev/null +++ b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/core/EncryptionMetadataTests.java @@ -0,0 +1,60 @@ +/* + * 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.core; + +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; +import org.junit.Assert; +import org.junit.Test; +import org.opensearch.encryption.MockKeyProvider; +import org.opensearch.encryption.frame.core.EncryptionMetadata; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +public class EncryptionMetadataTests { + + @Test + public void testCommitmentPolicy() { + CryptoAlgorithm cryptoAlgorithm = CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY; + CommitmentPolicy commitmentPolicy = CommitmentPolicy.ForbidEncryptAllowDecrypt; + EncryptionMaterialsRequest.Builder requestBuilder = EncryptionMaterialsRequest.newBuilder() + .setContext(new HashMap<>()) + .setRequestedAlgorithm(cryptoAlgorithm) + .setPlaintextSize(0) // To avoid skipping cache + .setCommitmentPolicy(commitmentPolicy); + + CachingCryptoMaterialsManager cachingMaterialsManager = CachingCryptoMaterialsManager.newBuilder() + .withMasterKeyProvider(new MockKeyProvider()) + .withCache(new LocalCryptoMaterialsCache(10)) + .withMaxAge(100, TimeUnit.MILLISECONDS) + .build(); + + Assert.assertThrows( + AwsCryptoException.class, + () -> new EncryptionMetadata(100, cachingMaterialsManager.getMaterialsForEncrypt(requestBuilder.build()), commitmentPolicy) + ); + + cryptoAlgorithm = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + requestBuilder.setRequestedAlgorithm(cryptoAlgorithm); + requestBuilder.setCommitmentPolicy(CommitmentPolicy.RequireEncryptRequireDecrypt); + Assert.assertThrows( + AwsCryptoException.class, + () -> new EncryptionMetadata( + 100, + cachingMaterialsManager.getMaterialsForEncrypt(requestBuilder.build()), + CommitmentPolicy.RequireEncryptRequireDecrypt + ) + ); + } +} diff --git a/libs/encryption-sdk/src/test/java/org/opensearch/encryption/frame/CryptoTests.java b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/frame/CryptoTests.java new file mode 100644 index 0000000000000..2171d70f9467d --- /dev/null +++ b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/frame/CryptoTests.java @@ -0,0 +1,499 @@ +/* + * 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 com.amazonaws.encryptionsdk.CommitmentPolicy; +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 com.amazonaws.encryptionsdk.internal.SignaturePolicy; +import org.junit.Assert; +import org.junit.Before; +import org.mockito.Mockito; +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.encryption.OffsetRangeFileInputStream; +import org.opensearch.encryption.frame.core.AwsCrypto; +import org.opensearch.encryption.frame.core.DecryptionHandler; +import org.opensearch.encryption.frame.core.EncryptionMetadata; +import org.opensearch.test.OpenSearchTestCase; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +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; + +public class CryptoTests extends OpenSearchTestCase { + + private static FrameCryptoProvider frameCryptoProvider; + + private static FrameCryptoProvider frameCryptoProviderTrailingAlgo; + + static class CustomFrameCryptoProviderTest extends FrameCryptoProvider { + private final int frameSize; + + CustomFrameCryptoProviderTest(AwsCrypto awsCrypto, HashMap config, int frameSize) { + super(awsCrypto, config); + this.frameSize = frameSize; + } + + @Override + public int getFrameSize() { + return frameSize; + } + } + + @Before + public void setupResources() { + frameCryptoProvider = new CustomFrameCryptoProviderTest( + createAwsCrypto(CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY), + new HashMap<>(), + 100 + ); + frameCryptoProviderTrailingAlgo = new CustomFrameCryptoProviderTest( + createAwsCrypto(CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_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 EncryptedStore { + byte[] encryptedContent; + long rawLength; + int encryptedLength; + File file; + } + + private EncryptedStore verifyAndGetEncryptedContent() throws IOException, URISyntaxException { + return verifyAndGetEncryptedContent(false, frameCryptoProvider); + } + + private EncryptedStore verifyAndGetEncryptedContent(boolean truncateRemainderPart, FrameCryptoProvider frameCryptoProvider) + throws IOException, URISyntaxException { + String path = CryptoTests.class.getResource("/raw_content_for_crypto_test").toURI().getPath(); + File file = new File(path); + + Object cryptoContext = frameCryptoProvider.initEncryptionMetadata(); + long length; + byte[] encryptedContent = new byte[1024 * 20]; + try (FileInputStream fileInputStream = new FileInputStream(file)) { + FileChannel channel = fileInputStream.getChannel(); + length = truncateRemainderPart ? channel.size() - (channel.size() % frameCryptoProvider.getFrameSize()) : channel.size(); + } + + int encLength = 0; + try (OffsetRangeFileInputStream inputStream = new OffsetRangeFileInputStream(file.toPath(), length, 0)) { + + InputStreamContainer stream = new InputStreamContainer(inputStream, length, 0); + InputStreamContainer encInputStream = frameCryptoProvider.createEncryptingStream(cryptoContext, stream); + assertNotNull(encInputStream); + + int readBytes; + while ((readBytes = encInputStream.getInputStream().read(encryptedContent, encLength, 1024)) != -1) { + encLength += readBytes; + } + } + + long calculatedEncryptedLength = frameCryptoProvider.estimateEncryptedLength(cryptoContext, length); + assertEquals(encLength, calculatedEncryptedLength); + + EncryptedStore encryptedStore = new EncryptedStore(); + encryptedStore.encryptedLength = encLength; + encryptedStore.encryptedContent = encryptedContent; + encryptedStore.rawLength = length; + encryptedStore.file = file; + return encryptedStore; + } + + 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, Long.MAX_VALUE / 2); + FrameCryptoProvider frameCryptoProvider = new CustomFrameCryptoProviderTest( + createAwsCrypto(CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY), + new HashMap<>(), + randomIntBetween(10, 10240) + ); + EncryptionMetadata cryptoContext = (EncryptionMetadata) frameCryptoProvider.initEncryptionMetadata(); + long encryptedLength = frameCryptoProvider.estimateEncryptedLength(cryptoContext, n); + ParsedCiphertext parsedCiphertext = new ParsedCiphertext(cryptoContext.getCiphertextHeaderBytes()); + long decryptedLength = frameCryptoProvider.estimateDecryptedLength(parsedCiphertext, encryptedLength); + assertEquals(n, decryptedLength); + } + } + + public void testSingleStreamEncryption() throws IOException, URISyntaxException { + + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStore.encryptedContent, + 0, + encryptedStore.encryptedLength + ); + long decryptedRawBytes = decryptAndVerify(byteArrayInputStream, encryptedStore.encryptedLength, encryptedStore.file); + assertEquals(encryptedStore.rawLength, decryptedRawBytes); + } + + public void testSingleStreamEncryptionTrailingSignatureAlgo() throws IOException, URISyntaxException { + + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(false, frameCryptoProviderTrailingAlgo); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStore.encryptedContent, + 0, + encryptedStore.encryptedLength + ); + long decryptedRawBytes = decryptAndVerify(byteArrayInputStream, encryptedStore.encryptedLength, encryptedStore.file); + assertEquals(encryptedStore.rawLength, decryptedRawBytes); + } + + public void testDecryptionOfCorruptedContent() throws IOException, URISyntaxException { + + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(); + encryptedStore.encryptedContent = "Corrupted content".getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStore.encryptedContent, + 0, + encryptedStore.encryptedLength + ); + + Assert.assertThrows( + BadCiphertextException.class, + () -> decryptAndVerify(byteArrayInputStream, encryptedStore.encryptedLength, encryptedStore.file) + ); + + } + + private long decryptAndVerify(InputStream encryptedStream, long encSize, File file) throws IOException { + FileInputStream inputStream = new FileInputStream(file); + long totalRawBytes = 0; + try (FileChannel channel = inputStream.getChannel()) { + channel.position(0); + + InputStream decryptingStream = frameCryptoProvider.createDecryptingStream(encryptedStream); + try (FileInputStream fis = new FileInputStream(file)) { + 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, actualBuffer.length); + assertEquals(readActualBytes, readBytes); + assertArrayEquals(actualBuffer, decryptedBuffer); + totalRawBytes += readActualBytes; + } + } + } + return totalRawBytes; + } + + public void testMultiPartStreamsEncryption() throws IOException, URISyntaxException { + Object cryptoContextObj = frameCryptoProvider.initEncryptionMetadata(); + EncryptionMetadata encryptionMetadata = (EncryptionMetadata) cryptoContextObj; + String path = CryptoTests.class.getResource("/raw_content_for_crypto_test").toURI().getPath(); + File file = new File(path); + byte[] encryptedContent = new byte[1024 * 20]; + int parts; + long partSize, lastPartSize; + long length; + try (FileInputStream inputStream = new FileInputStream(file); FileChannel channel = inputStream.getChannel()) { + length = channel.size(); + } + partSize = getPartSize(length, frameCryptoProvider.getFrameSize()); + parts = numberOfParts(length, partSize); + lastPartSize = length - (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 = frameCryptoProvider.createEncryptingStreamOfPart( + cryptoContextObj, + 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 += frameCryptoProvider.getTrailingSignatureLength(encryptionMetadata); + + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encryptedContent, 0, encLength); + decryptAndVerify(byteArrayInputStream, encLength, file); + + } + + 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 { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(); + assertTrue( + "This test is meant for file size not exactly divisible by frame size", + (encryptedStore.rawLength & frameCryptoProvider.getFrameSize()) != 0 + ); + validateBlockDownload(encryptedStore, 0, (int) encryptedStore.rawLength - 1); + } + + public void testBlockBasedDecryptionForEntireFileWithLinedUpFrameAlongFileBoundary() throws Exception { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(true, frameCryptoProvider); + assertEquals( + "This test is meant for file size exactly divisible by frame size", + 0, + (encryptedStore.rawLength % frameCryptoProvider.getFrameSize()) + ); + validateBlockDownload(encryptedStore, 0, (int) encryptedStore.rawLength - 1); + } + + public void testCorruptedTrailingSignature() throws IOException, URISyntaxException { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(false, frameCryptoProviderTrailingAlgo); + byte[] trailingData = "corrupted".getBytes(StandardCharsets.UTF_8); + byte[] corruptedTrailingContent = Arrays.copyOf( + encryptedStore.encryptedContent, + encryptedStore.encryptedContent.length + trailingData.length + ); + System.arraycopy(trailingData, 0, corruptedTrailingContent, encryptedStore.encryptedContent.length, trailingData.length); + encryptedStore.encryptedContent = corruptedTrailingContent; + encryptedStore.encryptedLength = corruptedTrailingContent.length; + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStore.encryptedContent, + 0, + encryptedStore.encryptedLength + ); + BadCiphertextException ex = Assert.assertThrows( + BadCiphertextException.class, + () -> decryptAndVerify(byteArrayInputStream, encryptedStore.encryptedLength, encryptedStore.file) + ); + Assert.assertEquals("Bad trailing signature", ex.getMessage()); + } + + public void testNoTrailingSignatureForTrailingAlgo() throws IOException, URISyntaxException { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(false, frameCryptoProviderTrailingAlgo); + Object cryptoContext = frameCryptoProviderTrailingAlgo.initEncryptionMetadata(); + int trailingLength = frameCryptoProvider.getTrailingSignatureLength(cryptoContext); + byte[] removedTrailingContent = Arrays.copyOf( + encryptedStore.encryptedContent, + encryptedStore.encryptedContent.length - trailingLength + ); + encryptedStore.encryptedContent = removedTrailingContent; + encryptedStore.encryptedLength = removedTrailingContent.length; + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + encryptedStore.encryptedContent, + 0, + encryptedStore.encryptedLength + ); + BadCiphertextException ex = Assert.assertThrows( + BadCiphertextException.class, + () -> decryptAndVerify(byteArrayInputStream, encryptedStore.encryptedLength, encryptedStore.file) + ); + Assert.assertEquals("Bad trailing signature", ex.getMessage()); + } + + public void testOutputSizeEstimateWhenHandlerIsNull() { + CryptoMaterialsManager cryptoMaterialsManager = Mockito.mock(CryptoMaterialsManager.class); + DecryptionHandler decryptionHandler = DecryptionHandler.create( + cryptoMaterialsManager, + CommitmentPolicy.RequireEncryptRequireDecrypt, + SignaturePolicy.AllowEncryptAllowDecrypt, + 1 + ); + 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 { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(); + int maxBlockNum = (int) encryptedStore.rawLength / frameCryptoProvider.getFrameSize(); + assert maxBlockNum > 5; + validateBlockDownload( + encryptedStore, + randomIntBetween(5, maxBlockNum / 2) * frameCryptoProvider.getFrameSize(), + randomIntBetween(maxBlockNum / 2 + 1, maxBlockNum) * frameCryptoProvider.getFrameSize() - 1 + ); + } + + public void testRandomRangeDecryption() throws Exception { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(); + // Testing for 100 iterations + for (int testIteration = 0; testIteration < 100; testIteration++) { + int startPos = randomIntBetween(0, (int) encryptedStore.rawLength - 1); + int endPos = randomIntBetween(startPos, (int) encryptedStore.rawLength - 1); + validateBlockDownload(encryptedStore, startPos, endPos); + } + } + + public void testDecryptionWithSameStartEndPos() throws Exception { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(); + int pos = randomIntBetween(0, (int) encryptedStore.rawLength - 1); + for (int testIteration = 0; testIteration < frameCryptoProvider.getFrameSize(); testIteration++) { + validateBlockDownload(encryptedStore, pos, pos); + } + } + + public void testBlockBasedDecryptionForLastBlock() throws Exception { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(); + int maxBlockNum = (int) encryptedStore.rawLength / frameCryptoProvider.getFrameSize(); + assert maxBlockNum > 5; + validateBlockDownload( + encryptedStore, + randomIntBetween(1, maxBlockNum - 1) * frameCryptoProvider.getFrameSize(), + (int) encryptedStore.rawLength - 1 + ); + } + + private void validateBlockDownload(EncryptedStore encryptedStore, int startPos, int endPos) throws Exception { + + EncryptedHeaderContentSupplier encryptedHeaderContentSupplier = createEncryptedHeaderContentSupplier( + encryptedStore.encryptedContent + ); + Object cryptoContext = frameCryptoProvider.loadEncryptionMetadata(encryptedHeaderContentSupplier); + DecryptedRangedStreamProvider decryptedStreamProvider = frameCryptoProvider.createDecryptingStreamOfRange( + cryptoContext, + startPos, + endPos + ); + + long[] transformedRange = decryptedStreamProvider.getAdjustedRange(); + int encryptedBlockSize = (int) (transformedRange[1] - transformedRange[0] + 1); + byte[] encryptedBlockBytes = new byte[encryptedBlockSize]; + System.arraycopy(encryptedStore.encryptedContent, (int) transformedRange[0], encryptedBlockBytes, 0, encryptedBlockSize); + ByteArrayInputStream encryptedStream = new ByteArrayInputStream(encryptedBlockBytes, 0, encryptedBlockSize); + InputStream decryptingStream = decryptedStreamProvider.getDecryptedStreamProvider().apply(encryptedStream); + + decryptAndVerifyBlock(decryptingStream, encryptedStore.file, startPos, endPos); + } + + public void testBlockBasedDecryptionForFirstBlock() throws Exception { + EncryptedStore encryptedStore = verifyAndGetEncryptedContent(); + // All block requests should properly line up with frames otherwise decryption will fail due to partial frames. + int blockEnd = randomIntBetween(5, (int) encryptedStore.rawLength / frameCryptoProvider.getFrameSize()) * frameCryptoProvider + .getFrameSize() - 1; + validateBlockDownload(encryptedStore, 0, blockEnd); + } + + private long decryptAndVerifyBlock(InputStream decryptedStream, File file, int rawContentStartPos, int rawContentEndPos) + throws IOException { + long totalRawBytes = 0; + + try (FileInputStream fis = new FileInputStream(file); FileChannel channel = fis.getChannel()) { + channel.position(rawContentStartPos); + byte[] decryptedBuffer = new byte[100]; + byte[] actualBuffer = new byte[100]; + + int readActualBytes; + int readBytes; + while ((readBytes = decryptedStream.read(decryptedBuffer, 0, decryptedBuffer.length)) != -1) { + readActualBytes = fis.read(actualBuffer, 0, Math.min(actualBuffer.length, rawContentEndPos - rawContentStartPos + 1)); + rawContentEndPos -= readActualBytes; + assertEquals(readActualBytes, readBytes); + assertArrayEquals(actualBuffer, decryptedBuffer); + totalRawBytes += readActualBytes; + } + } + return totalRawBytes; + } + + public void testEmptyContentCrypto() throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[] {}); + Object cryptoContext = frameCryptoProvider.initEncryptionMetadata(); + InputStreamContainer stream = new InputStreamContainer(byteArrayInputStream, 0, 0); + InputStreamContainer encryptingStream = frameCryptoProvider.createEncryptingStream(cryptoContext, stream); + InputStream decryptingStream = frameCryptoProvider.createDecryptingStream(encryptingStream.getInputStream()); + decryptingStream.readAllBytes(); + } + + public void testEmptyContentCryptoTrailingSignatureAlgo() throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[] {}); + Object cryptoContext = frameCryptoProviderTrailingAlgo.initEncryptionMetadata(); + InputStreamContainer stream = new InputStreamContainer(byteArrayInputStream, 0, 0); + InputStreamContainer encryptingStream = frameCryptoProviderTrailingAlgo.createEncryptingStream(cryptoContext, stream); + InputStream decryptingStream = frameCryptoProviderTrailingAlgo.createDecryptingStream(encryptingStream.getInputStream()); + decryptingStream.readAllBytes(); + } + +} diff --git a/libs/encryption-sdk/src/test/java/org/opensearch/encryption/frame/TrimmingStreamTests.java b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/frame/TrimmingStreamTests.java new file mode 100644 index 0000000000000..1fe99755aa465 --- /dev/null +++ b/libs/encryption-sdk/src/test/java/org/opensearch/encryption/frame/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.frame; + +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/libs/encryption-sdk/src/test/resources/encrypted_key b/libs/encryption-sdk/src/test/resources/encrypted_key new file mode 100644 index 0000000000000..da4e503581585 --- /dev/null +++ b/libs/encryption-sdk/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/libs/encryption-sdk/src/test/resources/raw_content_for_crypto_test b/libs/encryption-sdk/src/test/resources/raw_content_for_crypto_test new file mode 100644 index 0000000000000..c93b6161ac8d6 --- /dev/null +++ b/libs/encryption-sdk/src/test/resources/raw_content_for_crypto_test @@ -0,0 +1,25 @@ +ewogICJmaWxlSW5mb3MiOiBbCiAgICB7CiAgICAgICJuYW1lIjogIl80LmZubSIsCiAgICAgICJyZW1vdGVfc +GF0aCI6ICIyYzYwMzNmNmZlZTY0NTY1YTU3YzQzZWVmZThmY2QzMS9kdW1teS1jb2xsZWN0aW9uMi9kMDRmYz +AyZi0wMDQ0LTRhYmYtYjgzMy0xMGE0YTA5M2VkNTcvMC8wL2luZGljZXMvMSIsCiAgICAgICJzaXplIjogOTQz +CiAgICB9LAogICAgewogICAgICAibmFtZSI6ICJfMl9MdWNlbmU4MF8wLmR2ZCIsCiAgICAgICJyZW1vdGVfcGF +0aCI6ICIyYzYwMzNmNmZlZTY0NTY1YTU3YzQzZWVmZThmY2QzMS9kdW1teS1jb2xsZWN0aW9uMi9kMDRmYzAyZi0wMDQ0LTRhYmYtYjg +zMy0xMGE0YTA5M2VkNTcvMC8wL2luZGljZXMvMSIsCiAgICAgICJzaXplIjogMzU1CiAgICB9CiAgXQp9 +ewogICJja3BfZmlsZSI6IHsKICAgICJuYW1lIjogInRyYW5zbG9nLTguY2twIiwKICAgICJyZW1vdGVfcGF0aCI6ICIyYz +YwMzNmNmZlZTY0NTY1YTU3YzQzZWVmZThmY2QzMS9kdW1teS1jb2xsZWN0aW9uMi9kMDRmYzAyZi0wMDQ0LTRhYmYtYjgzMy0 +xMGE0YTA5M2VkNTcvMC8wL3RyYW5zbG9nLzEiLAogICAgInNpemUiOiAwCiAgfSwKICAidGxvZ192ZXJzaW9uIjogewogICAgIjg +iOiAiMmM2MDMzZjZmZWU2NDU2NWE1N2M0M2VlZmU4ZmNkMzEvZHVtbXktY29sbGVjdGlvbjIvZDA0ZmMwMmYtMDA0NC00YWJmLWI4MzMtMT +BhNGEwOTNlZDU3LzAvMC90cmFuc2xvZy8xIgogIH0KfQ== +ewogICJmaWxlSW5mb3MiOiBbCiAgICB7CiAgICAgICJuYW1lIjogIl80LmZubSIsCiAgICAgICJyZW1vdGVfcGF0aCI6ICIyYzYwMzNmNmZl +ZTY0NTY1YTU3YzQzZWVmZThmY2QzMS9kdW1teS1jb2xsZWN0aW9uMi9kMDRmYzAyZi0wMDQ0LTRhYmYtYjgzMy0xMGE0YTA5M2VkNTcvMC8wL2luZG +ljZXMvMSIsCiAgICAgICJzaXplIjogOTQzCiAgICB9LAogICAgewogICAgICAibmFtZSI6ICJfNC5mZHQiLAogICAgICAicmVtb3RlX3BhdGgiOiAi +MmM2MDMzZjZmZWU2NDU2NWE1N2M0M2VlZmU4ZmNkMzEvZHVtbXktY29sbGVjdGlvbjIvZDA0ZmMwMmYtMDA0NC00YWJmLWI4MzMtMTBhNGEwOTNlZDU3 +LzAvMC9pbmRpY2VzLzEiLAogICAgICAic2l6ZSI6IDQ1MTMKICAgIH0sCiAgICB7CiAgICAgICJuYW1lIjogInNlZ21lbnRzX2MiLAogICAgICAicmVtb3R +lX3BhdGgiOiAiMmM2MDMzZjZmZWU2NDU2NWE1N2M0M2VlZmU4ZmNkMzEvZHVtbXktY29sbGVjdGlvbjIvZDA0ZmMwMmYtMDA0NC00YWJmLWI4MzM +tMTBhNGEwOTNlZDU3LzAvMC9pbmRpY2VzLzEiLAogICAgICAic2l6ZSI6IDM1NQogICAgfQogIF0KfQ== +ewogICJja3BfZmlsZSI6IHsKICAgICJuYW1lIjogInRyYW5zbG9nLTcuY2twIiwKICAgICJyZW1vdGVfcGF0aCI6ICIyYzYwMzNmNmZlZ +TY0NTY1YTU3YzQzZWVmZThmY2QzMS9kdW1teS1jb2xsZWN0aW9uMi9kMDRmYzAyZi0wMDQ0LTRhYmYtYjgzMy0xMGE0YTA5M2VkNTcvMC8wL3RyY +W5zbG9nLzEiLAogICAgInNpemUiOiAwCiAgfSwKICAidGxvZ192ZXJzaW9uIjogewogICAgIjYiOiAiMmM2MDMzZjZmZWU2NDU2NWE1N2M0M2VlZ +mU4ZmNkMzEvZHVtbXktY29sbGVjdGlvbjIvZDA0ZmMwMmYtMDA0NC00YWJmLWI4MzMtMTBhNGEwOTNlZDU3LzAvMC90cmFuc2xvZy8xIiwKICAgICI3Ijo +gIjJjNjAzM2Y2ZmVlNjQ1NjVhNTdjNDNlZWZlOGZjZDMxL2R1bW15LWNvbGxlY3Rpb24yL2QwNGZjMDJmLTAwNDQtNGFiZi1iODMzLTEwYTRhMDkzZW +Q1Ny8wLzAvdHJhbnNsb2cvMSIKICB9Cn0= + diff --git a/libs/encryption-sdk/src/test/resources/raw_key b/libs/encryption-sdk/src/test/resources/raw_key new file mode 100644 index 0000000000000..3c4f8b54cbb6a --- /dev/null +++ b/libs/encryption-sdk/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