| // Copyright 2012 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // 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. |
| |
| package com.google.enterprise.adaptor; |
| |
| import java.nio.charset.Charset; |
| import java.security.InvalidKeyException; |
| import java.security.Key; |
| import java.security.KeyPair; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.Random; |
| |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.Cipher; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.NoSuchPaddingException; |
| import javax.crypto.SecretKey; |
| import javax.crypto.spec.SecretKeySpec; |
| import javax.xml.bind.DatatypeConverter; |
| |
| /** |
| * Provides encoding and decoding of sensitive values. It supports plain text, |
| * obfuscated, and encrypted formats. |
| * |
| * <p>This class is thread-safe. |
| */ |
| class SensitiveValueCodec implements SensitiveValueDecoder { |
| private static final SecretKey OBFUSCATING_KEY = new SecretKeySpec( |
| new byte[] { |
| (byte) 0x7d, (byte) 0xec, (byte) 0xbd, (byte) 0x31, (byte) 0x4e, |
| (byte) 0xf3, (byte) 0x68, (byte) 0x69, (byte) 0x69, (byte) 0x78, |
| (byte) 0x7a, (byte) 0xc9, (byte) 0xfc, (byte) 0x99, (byte) 0x07, |
| (byte) 0x9c |
| }, "AES"); |
| private static final Charset CHARSET = Charset.forName("UTF-8"); |
| |
| private final KeyPair encryptingKey; |
| private final Random random = new Random(); |
| |
| /** |
| * Construct a codec capable of encoding and decoding secrets. The provided |
| * {@code encryptingKey} is used when encryption is requested, but may be |
| * {@code null}. If {@code null}, then {@link SecurityLevel#ENCRYPTED} will be |
| * unavailable for encryption and decryption. |
| * |
| * @param encryptingKey key used when encryption is requested, or {@code null} |
| */ |
| public SensitiveValueCodec(KeyPair encryptingKey) { |
| this.encryptingKey = encryptingKey; |
| } |
| |
| private Cipher createObfuscatingCipher() { |
| try { |
| return Cipher.getInstance(OBFUSCATING_KEY.getAlgorithm()); |
| } catch (NoSuchAlgorithmException ex) { |
| throw new AssertionError(); |
| } catch (NoSuchPaddingException ex) { |
| throw new AssertionError(); |
| } |
| } |
| |
| private Cipher createEncryptingCipher() { |
| try { |
| return Cipher.getInstance(encryptingKey.getPrivate().getAlgorithm()); |
| } catch (NoSuchAlgorithmException ex) { |
| throw new IllegalStateException(ex); |
| } catch (NoSuchPaddingException ex) { |
| throw new IllegalStateException(ex); |
| } |
| } |
| |
| /** |
| * Encode {@code readable} using requested {@code security}. |
| * |
| * @return encoded non-readable of {@code readable} |
| * @throws IllegalStateException if the encrypting key and encrypting cipher |
| * are misconfigured or not in agreement, or is requested when unavailable |
| */ |
| public String encodeValue(String readable, SecurityLevel security) { |
| String encoded; |
| switch (security) { |
| case PLAIN_TEXT: |
| // We always apply the prefix, even if it isn't strictly necessary. This |
| // is to make it obvious that the process actually did something to the |
| // user and makes them more aware of the pl: prefix if they need to use |
| // it. |
| encoded = readable; |
| break; |
| |
| case OBFUSCATED: |
| encoded = encryptAndBase64(readable, createObfuscatingCipher(), |
| OBFUSCATING_KEY); |
| break; |
| |
| case ENCRYPTED: |
| if (encryptingKey == null) { |
| throw new IllegalStateException( |
| "No key provided to encrypt value"); |
| } |
| encoded = encryptAndBase64(readable, createEncryptingCipher(), |
| encryptingKey.getPublic()); |
| break; |
| |
| default: |
| throw new AssertionError(); |
| } |
| return security.getPrefix() + encoded; |
| } |
| |
| /** |
| * Beware that the provided Cipher will be modified as part of encryption. |
| */ |
| private String encryptAndBase64(String readable, Cipher cipher, Key key) { |
| byte[] bytes = readable.getBytes(CHARSET); |
| |
| try { |
| cipher.init(Cipher.ENCRYPT_MODE, key); |
| } catch (InvalidKeyException ex) { |
| throw new AssertionError(); |
| } |
| try { |
| bytes = cipher.doFinal(bytes); |
| } catch (IllegalBlockSizeException ex) { |
| // The algorithm does not seem suited for our use. |
| throw new IllegalStateException(ex); |
| } catch (BadPaddingException ex) { |
| throw new AssertionError(); |
| } |
| |
| return DatatypeConverter.printBase64Binary(bytes); |
| } |
| |
| /** |
| * Determine what encode operation was used to produce {@code nonReadable}. |
| * |
| * @param nonReadable previously-encoded string |
| * @return security used |
| */ |
| public SecurityLevel determineSecurityLevelUsed(String nonReadable) { |
| SecurityLevel security = SecurityLevel.PLAIN_TEXT; |
| for (SecurityLevel trySecurityLevel : SecurityLevel.values()) { |
| if (nonReadable.startsWith(trySecurityLevel.getPrefix())) { |
| security = trySecurityLevel; |
| break; |
| } |
| } |
| return security; |
| } |
| |
| /** |
| * Reverse previous encode operation that produced {@code nonReadable}. |
| * |
| * @param nonReadable previously-encoded string |
| * @return non-encoded string |
| * @throws IllegalArgumentException if {@code nonReadable} is unable to be |
| * decoded |
| */ |
| @Override |
| public String decodeValue(String nonReadable) { |
| SecurityLevel security = determineSecurityLevelUsed(nonReadable); |
| if (nonReadable.startsWith(security.getPrefix())) { |
| nonReadable = nonReadable.substring(security.getPrefix().length()); |
| } |
| switch (security) { |
| case PLAIN_TEXT: |
| return nonReadable; |
| |
| case OBFUSCATED: |
| return base64AndDecrypt(nonReadable, createObfuscatingCipher(), |
| OBFUSCATING_KEY); |
| |
| case ENCRYPTED: |
| if (encryptingKey == null) { |
| throw new IllegalArgumentException( |
| "No key provided to decrypt value"); |
| } |
| return base64AndDecrypt(nonReadable, createEncryptingCipher(), |
| encryptingKey.getPrivate()); |
| |
| default: |
| throw new AssertionError(); |
| } |
| } |
| |
| /** |
| * Beware that the provided Cipher will be modified as part of decryption. |
| */ |
| private String base64AndDecrypt(String nonReadable, Cipher cipher, Key key) { |
| byte[] bytes = DatatypeConverter.parseBase64Binary(nonReadable); |
| |
| try { |
| cipher.init(Cipher.DECRYPT_MODE, key); |
| } catch (InvalidKeyException ex) { |
| throw new AssertionError(); |
| } |
| try { |
| bytes = cipher.doFinal(bytes); |
| } catch (IllegalBlockSizeException ex) { |
| throw new AssertionError(); |
| } catch (BadPaddingException ex) { |
| throw new IllegalArgumentException(ex); |
| } |
| |
| return new String(bytes, CHARSET); |
| } |
| |
| /** |
| * Possible levels of security for storing value. |
| */ |
| public enum SecurityLevel { |
| /** |
| * The value is prefixed with "pl:", but is otherwise left as-is. |
| */ |
| PLAIN_TEXT("pl:"), |
| /** |
| * The value is prefixed with "obf:" and is obfuscated, but no real security |
| * is added. AES is used to encrypt the value, but the key is hard-coded. |
| */ |
| OBFUSCATED("obf:"), |
| /** |
| * The value is prefixed with "pkc:" and is encrypted using the public key |
| * cryptography provided to the constructor. |
| */ |
| ENCRYPTED("pkc:"), |
| ; |
| |
| private final String prefix; |
| |
| private SecurityLevel(String prefix) { |
| this.prefix = prefix; |
| } |
| |
| String getPrefix() { |
| return prefix; |
| } |
| } |
| } |