From d9b5059508e1bb2b01496572468bff73c3eb59bc Mon Sep 17 00:00:00 2001 From: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Date: Wed, 26 Oct 2022 15:47:55 -0400 Subject: [PATCH] [Feature/Identity] Allow for Encryption/Decryption of Principals into PITs (#4730) Allow for Encryption/Decryption of Principals into PITs Signed-off-by: Stephen Crawford Signed-off-by: Stephen Crawford --- CHANGELOG.md | 2 + .../internal/fake_git/remote/settings.gradle | 1 - .../identity/ExtensionTokenProcessor.java | 166 ++++++++++++++---- .../identity/PrincipalIdentifierToken.java | 1 + .../ExtensionTokenProcessorTests.java | 132 +++++++++++--- .../PrincipalIdentifierTokenTests.java | 50 ++++-- 6 files changed, 277 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b70616f1ac2..a73a9f3e7b8e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -196,6 +196,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Addition of Missing Value feature in the GeoShape Aggregations. - Install and configure Log4j JUL Adapter for Lucene 9.4 ([#4754](https://github.com/opensearch-project/OpenSearch/pull/4754)) - Added feature to ignore indexes starting with dot during shard limit validation.([#4695](https://github.com/opensearch-project/OpenSearch/pull/4695)) +- Added encryption/decryption for principal identifier tokens with extensions ([#4730](https://github.com/opensearch-project/OpenSearch/pull/4730)) +- ### Changed ### Deprecated ### Removed diff --git a/buildSrc/src/integTest/resources/org/opensearch/gradle/internal/fake_git/remote/settings.gradle b/buildSrc/src/integTest/resources/org/opensearch/gradle/internal/fake_git/remote/settings.gradle index a4aaba7133b81..323a05fb870c2 100644 --- a/buildSrc/src/integTest/resources/org/opensearch/gradle/internal/fake_git/remote/settings.gradle +++ b/buildSrc/src/integTest/resources/org/opensearch/gradle/internal/fake_git/remote/settings.gradle @@ -27,7 +27,6 @@ * specific language governing permissions and limitations * under the License. */ - include ":distribution:bwc:bugfix" include ":distribution:bwc:minor" include ":distribution:archives:darwin-tar" diff --git a/server/src/main/java/org/opensearch/identity/ExtensionTokenProcessor.java b/server/src/main/java/org/opensearch/identity/ExtensionTokenProcessor.java index a09b425b6cda5..dde74111cdb19 100644 --- a/server/src/main/java/org/opensearch/identity/ExtensionTokenProcessor.java +++ b/server/src/main/java/org/opensearch/identity/ExtensionTokenProcessor.java @@ -2,84 +2,182 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - package org.opensearch.identity; import java.security.Principal; +import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.AEADBadTagException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; /** * Token processor class to handle token encryption/decryption * This processor is will be instantiated for every extension */ public class ExtensionTokenProcessor { + public static final String INVALID_TOKEN_MESSAGE = "Token must not be null and must be a colon-separated String"; public static final String INVALID_EXTENSION_MESSAGE = "Token passed here is for a different extension"; + public static final String INVALID_ALGO_MESSAGE = "Failed to create a token because an invalid hashing algorithm was used."; + public static final String INVALID_PRINCIPAL_MESSAGE = "Principal passed here does not have a name."; + public static final String INVALID_KEY_MESSAGE = "Could not verify the authenticity of the provided key."; + public static final String INVALID_TAG_MESSAGE = "Token extraction could not be processed because of an invalid tag."; + public static final int KEY_SIZE_BITS = 256; + public static final int INITIALIZATION_VECTOR_SIZE_BYTES = 96; + public static final int TAG_LENGTH_BITS = 128; + + public final String extensionUniqueId; - private final String extensionUniqueId; + private SecretKey secretKey; + private SecretKeySpec secretKeySpec; + private Cipher encryptionCipher; public ExtensionTokenProcessor(String extensionUniqueId) { + + // The extension ID should ALWAYS stay the same this.extensionUniqueId = extensionUniqueId; + + } + + /** + * Allow for the reseting of the extension processor key. This will remove all access to existing encryptions. + */ + public SecretKey generateKey() throws NoSuchAlgorithmException { + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(KEY_SIZE_BITS, SecureRandom.getInstanceStrong()); + this.secretKey = keyGen.generateKey(); + return this.secretKey; + } + + /** + * Getter for the extensionTokenProcessor's secretKey + */ + public SecretKey getSecretKey() { + + return this.secretKey; } - public String getExtensionUniqueId() { - return extensionUniqueId; + /** + * Creates a new initialization vector for encryption--CAN ONLY BE USED ONCE PER KEY + */ + public byte[] generateInitializationVector() { + + byte[] initializationVector = new byte[INITIALIZATION_VECTOR_SIZE_BYTES]; + SecureRandom random = new SecureRandom(); + random.nextBytes(initializationVector); + return initializationVector; } /** * Create a two-way encrypted access token for given principal for this extension - * @return token generated from principal */ - public PrincipalIdentifierToken generateToken(Principal principal) { - // This is a placeholder implementation - // More concrete implementation will be covered in https://github.com/opensearch-project/OpenSearch/issues/4485 - String token = principal.getName() + ":" + extensionUniqueId; + public PrincipalIdentifierToken generateToken(Principal principal) throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException { + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(this.extensionUniqueId.getBytes(StandardCharsets.UTF_8)); + try { + output.write(principal.getName().getBytes(StandardCharsets.UTF_8)); + } catch (NullPointerException ex) { + throw new NullPointerException(INVALID_PRINCIPAL_MESSAGE); + } + + byte[] combinedAttributes = output.toByteArray(); + + SecretKey secretKey = generateKey(); + byte[] initializationVector = generateInitializationVector(); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - return new PrincipalIdentifierToken(token); + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BITS, initializationVector); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec); + + byte[] combinedEncoding = cipher.doFinal(combinedAttributes); + + byte[] combinedEncodingWithIV = ByteBuffer.allocate(INITIALIZATION_VECTOR_SIZE_BYTES + combinedEncoding.length) + .put(initializationVector) + .put(combinedEncoding) + .array(); + + String s = Base64.getEncoder().encodeToString(combinedEncodingWithIV); + return new PrincipalIdentifierToken(s); } /** * Decrypt the token and extract Principal - * @param token the requester identity token, should not be null - * @return Principal - * * @opensearch.internal - * - * This method contains a placeholder implementation. - * More concrete implementation will be covered in https://github.com/opensearch-project/OpenSearch/issues/4485 */ - public Principal extractPrincipal(PrincipalIdentifierToken token) throws IllegalArgumentException { - // check is token is valid, we don't do anything if it is valid - // else we re-throw the thrown exception - validateToken(token); - - String[] parts = token.getToken().split(":"); - final String principalName = parts[0]; - return () -> principalName; + public String extractPrincipal(PrincipalIdentifierToken token, SecretKey secretKey) throws IllegalArgumentException, + InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException { + + String tokenString = token.getToken(); + byte[] tokenBytes = Base64.getDecoder().decode(tokenString); + + ByteBuffer bb = ByteBuffer.wrap(tokenBytes); + + byte[] iv = new byte[INITIALIZATION_VECTOR_SIZE_BYTES]; + bb.get(iv); + + byte[] cipherText = new byte[bb.remaining()]; + bb.get(cipherText); + + Cipher decryptionCipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BITS, iv); + + try { + decryptionCipher.init(Cipher.DECRYPT_MODE, secretKey, spec); + } catch (InvalidKeyException ex) { + throw new InvalidKeyException(INVALID_KEY_MESSAGE); + } + + byte[] combinedEncoding; + try { + combinedEncoding = decryptionCipher.doFinal(cipherText); + } catch (AEADBadTagException ex) { + throw new AEADBadTagException(INVALID_TAG_MESSAGE); + } + + String decodedPrincipal = new String(combinedEncoding, StandardCharsets.UTF_8).replace(this.extensionUniqueId, ""); + String decodedExtensionsID = new String(combinedEncoding, StandardCharsets.UTF_8).replace(decodedPrincipal, ""); + if (decodedExtensionsID.equals(this.extensionUniqueId) == false) { + throw new IllegalArgumentException(INVALID_EXTENSION_MESSAGE); + } + return decodedPrincipal; } /** * Checks validity of the requester identifier token - * @param token The requester identifier token - * @throws IllegalArgumentException when token is invalid * * This method contains a placeholder implementation. * More concrete implementation will be covered in https://github.com/opensearch-project/OpenSearch/issues/4485 */ public void validateToken(PrincipalIdentifierToken token) throws IllegalArgumentException { + // Check whether token exists if (token == null || token.getToken() == null) { throw new IllegalArgumentException(INVALID_TOKEN_MESSAGE); } - String[] parts = token.getToken().split(":"); - - // check whether token is malformed - if (parts.length != 2) { + String tokenName = token.getWriteableName(); + byte[] tokenBytes = Base64.getDecoder().decode(token.getToken()); + // Check whether token is correct length + if (tokenBytes.length >= INITIALIZATION_VECTOR_SIZE_BYTES) { throw new IllegalArgumentException(INVALID_TOKEN_MESSAGE); } - // check whether token is for this extension - if (!parts[1].equals(extensionUniqueId)) { - throw new IllegalArgumentException(INVALID_EXTENSION_MESSAGE); - } } } diff --git a/server/src/main/java/org/opensearch/identity/PrincipalIdentifierToken.java b/server/src/main/java/org/opensearch/identity/PrincipalIdentifierToken.java index dfe9ef7135edd..8abc611868500 100644 --- a/server/src/main/java/org/opensearch/identity/PrincipalIdentifierToken.java +++ b/server/src/main/java/org/opensearch/identity/PrincipalIdentifierToken.java @@ -28,6 +28,7 @@ public class PrincipalIdentifierToken implements NamedWriteable { */ protected PrincipalIdentifierToken(String token) { this.token = token; + } public PrincipalIdentifierToken(StreamInput in) throws IOException { diff --git a/server/src/test/java/org/opensearch/identity/ExtensionTokenProcessorTests.java b/server/src/test/java/org/opensearch/identity/ExtensionTokenProcessorTests.java index 606082c1dc52e..1b6183bcd58f8 100644 --- a/server/src/test/java/org/opensearch/identity/ExtensionTokenProcessorTests.java +++ b/server/src/test/java/org/opensearch/identity/ExtensionTokenProcessorTests.java @@ -9,69 +9,153 @@ import java.security.Principal; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.AEADBadTagException; +import javax.crypto.SecretKey; + public class ExtensionTokenProcessorTests extends OpenSearchTestCase { - private static final Principal userPrincipal = () -> "user1"; + private static final String userName = "user1"; + private static final Principal userPrincipal = () -> userName; public void testGenerateToken() { + String extensionUniqueId = "ext_1"; ExtensionTokenProcessor extensionTokenProcessor = new ExtensionTokenProcessor(extensionUniqueId); - String expectedToken = userPrincipal.getName() + ":" + extensionUniqueId; - PrincipalIdentifierToken generatedIdentifier = extensionTokenProcessor.generateToken(userPrincipal); + PrincipalIdentifierToken generatedIdentifier; + + try { + generatedIdentifier = extensionTokenProcessor.generateToken(userPrincipal); + } catch (InvalidKeyException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | IOException | NoSuchPaddingException + | IllegalBlockSizeException | BadPaddingException e) { + + throw new Error(e); + } assertNotEquals(null, generatedIdentifier); - assertEquals(expectedToken, generatedIdentifier.getToken()); } public void testExtractPrincipal() { - String extensionUniqueId = "ext_1"; - String token = userPrincipal.getName() + ":" + extensionUniqueId; - PrincipalIdentifierToken principalIdentifierToken = new PrincipalIdentifierToken(token); + String extensionUniqueId = "ext_2"; ExtensionTokenProcessor extensionTokenProcessor = new ExtensionTokenProcessor(extensionUniqueId); - Principal principal = extensionTokenProcessor.extractPrincipal(principalIdentifierToken); + PrincipalIdentifierToken generatedIdentifier; + + String principalName; + + SecretKey secretKey; + + try { + generatedIdentifier = extensionTokenProcessor.generateToken(userPrincipal); + secretKey = extensionTokenProcessor.getSecretKey(); + principalName = extensionTokenProcessor.extractPrincipal(generatedIdentifier, secretKey); + } catch (InvalidKeyException | IllegalArgumentException | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException | IOException e) { - assertNotEquals(null, principal); - assertEquals(userPrincipal.getName(), principal.getName()); + throw new Error(e); + } + + assertEquals(userName, principalName); } - public void testExtractPrincipalMalformedToken() { - String extensionUniqueId = "ext_1"; - String token = "garbage"; - PrincipalIdentifierToken principalIdentifierToken = new PrincipalIdentifierToken(token); + public void testBadAEADTag() { + + String extensionUniqueId = "ext_2"; + ExtensionTokenProcessor extensionTokenProcessor = new ExtensionTokenProcessor(extensionUniqueId); + PrincipalIdentifierToken generatedIdentifier; + + // Create a new key that does not match + SecretKey secretKey; + try { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(128, SecureRandom.getInstanceStrong()); + secretKey = keyGen.generateKey(); + generatedIdentifier = extensionTokenProcessor.generateToken(userPrincipal); + } catch (InvalidKeyException | IllegalArgumentException | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException | IOException ex) { + + throw new Error(ex); + } + Exception exception = assertThrows( - IllegalArgumentException.class, - () -> extensionTokenProcessor.extractPrincipal(principalIdentifierToken) + AEADBadTagException.class, + () -> extensionTokenProcessor.extractPrincipal(generatedIdentifier, secretKey) ); assertFalse(exception.getMessage().isEmpty()); - assertEquals(ExtensionTokenProcessor.INVALID_TOKEN_MESSAGE, exception.getMessage()); + assertEquals(ExtensionTokenProcessor.INVALID_TAG_MESSAGE, exception.getMessage()); } - public void testExtractPrincipalWithNullToken() { + public void testBadKey() { + + String extensionUniqueId = "ext_2"; + + ExtensionTokenProcessor extensionTokenProcessor = new ExtensionTokenProcessor(extensionUniqueId); + + PrincipalIdentifierToken generatedIdentifier; + + String principalName; + + // Create a key of the wrong type + SecretKey secretKey; + try { + KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5"); + keyGen.init(128, SecureRandom.getInstanceStrong()); + secretKey = keyGen.generateKey(); + generatedIdentifier = extensionTokenProcessor.generateToken(userPrincipal); + } catch (InvalidKeyException | IllegalArgumentException | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException | IOException ex) { + + throw new Error(ex); + } + + Exception exception = assertThrows( + InvalidKeyException.class, + () -> extensionTokenProcessor.extractPrincipal(generatedIdentifier, secretKey) + ); + + assertFalse(exception.getMessage().isEmpty()); + assertEquals(ExtensionTokenProcessor.INVALID_KEY_MESSAGE, exception.getMessage()); + } + + public void testGeneratePrincipalWithNullPrincipal() { String extensionUniqueId1 = "ext_1"; ExtensionTokenProcessor extensionTokenProcessor = new ExtensionTokenProcessor(extensionUniqueId1); - Exception exception = assertThrows(IllegalArgumentException.class, () -> extensionTokenProcessor.extractPrincipal(null)); + Exception exception = assertThrows(NullPointerException.class, () -> extensionTokenProcessor.generateToken(null)); assertFalse(exception.getMessage().isEmpty()); - assertEquals(ExtensionTokenProcessor.INVALID_TOKEN_MESSAGE, exception.getMessage()); + assertEquals(ExtensionTokenProcessor.INVALID_PRINCIPAL_MESSAGE, exception.getMessage()); } - public void testExtractPrincipalWithTokenInvalidExtension() { + public void testExtractPrincipalWithTokenInvalidExtension() throws InvalidAlgorithmParameterException, NoSuchPaddingException, + IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, IOException, InvalidKeyException { String extensionUniqueId1 = "ext_1"; String extensionUniqueId2 = "ext_2"; String token = userPrincipal.getName() + ":" + extensionUniqueId1; - PrincipalIdentifierToken principalIdentifierToken = new PrincipalIdentifierToken(token); - ExtensionTokenProcessor extensionTokenProcessor = new ExtensionTokenProcessor(extensionUniqueId2); + + ExtensionTokenProcessor extensionTokenProcessor1 = new ExtensionTokenProcessor(extensionUniqueId1); + ExtensionTokenProcessor extensionTokenProcessor2 = new ExtensionTokenProcessor(extensionUniqueId2); + + PrincipalIdentifierToken generatedIdentifier2 = extensionTokenProcessor2.generateToken(userPrincipal); + SecretKey secretKey2 = extensionTokenProcessor2.getSecretKey(); Exception exception = assertThrows( IllegalArgumentException.class, - () -> extensionTokenProcessor.extractPrincipal(principalIdentifierToken) + () -> extensionTokenProcessor1.extractPrincipal(generatedIdentifier2, secretKey2) ); assertFalse(exception.getMessage().isEmpty()); diff --git a/server/src/test/java/org/opensearch/identity/PrincipalIdentifierTokenTests.java b/server/src/test/java/org/opensearch/identity/PrincipalIdentifierTokenTests.java index 7ff432715fc1f..697f0799ad6ed 100644 --- a/server/src/test/java/org/opensearch/identity/PrincipalIdentifierTokenTests.java +++ b/server/src/test/java/org/opensearch/identity/PrincipalIdentifierTokenTests.java @@ -5,33 +5,51 @@ package org.opensearch.identity; -import org.opensearch.common.bytes.BytesReference; -import org.opensearch.common.io.stream.BytesStreamInput; -import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.test.OpenSearchTestCase; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + public class PrincipalIdentifierTokenTests extends OpenSearchTestCase { public void testInstantiationWithStreamInput() throws IOException { String extensionUniqueId = "ext_1"; Principal principal = () -> "user"; ExtensionTokenProcessor extensionTokenProcessor = new ExtensionTokenProcessor(extensionUniqueId); - PrincipalIdentifierToken principalIdentifierToken = extensionTokenProcessor.generateToken(principal); - - String expectedToken = principal.getName() + ":" + extensionUniqueId; - assertEquals(expectedToken, principalIdentifierToken.getToken()); - - try (BytesStreamOutput out = new BytesStreamOutput()) { - principalIdentifierToken.writeTo(out); - out.flush(); - try (BytesStreamInput in = new BytesStreamInput(BytesReference.toBytes(out.bytes()))) { - principalIdentifierToken = new PrincipalIdentifierToken(in); - - assertEquals(expectedToken, principalIdentifierToken.getToken()); - } + PrincipalIdentifierToken principalIdentifierToken; + SecretKey secretKey; + String extractedTokenOne; + + try { + principalIdentifierToken = extensionTokenProcessor.generateToken(principal); + secretKey = extensionTokenProcessor.getSecretKey(); + extractedTokenOne = extensionTokenProcessor.extractPrincipal(principalIdentifierToken, secretKey); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException + | IllegalBlockSizeException | BadPaddingException e) { + + throw new Error(e); } + + String expectedToken = principal.getName(); + assertEquals(expectedToken, extractedTokenOne); + + // TODO: Implement this so it compares the same input generating the same token via stream + // try (BytesStreamOutput out = new BytesStreamOutput()) { + // principalIdentifierToken.writeTo(out); + // out.flush(); + // try (BytesStreamInput in = new BytesStreamInput(BytesReference.toBytes(out.bytes()))) { + // principalIdentifierToken = new PrincipalIdentifierToken(in); + // + // assertEquals(expectedToken, principalIdentifierToken.getToken()); + // } + // } } }