| // 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; | |
| } | |
| } | |
| } |