blob: 32ccb5013f3106fa838e5248ad67868ab2a98f29 [file] [log] [blame]
// 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;
}
}
}