將敏感對象發送出信任區域前進行簽名並加密
敏感數據傳輸過程中要防止竊取和惡意篡改。使用安全的加密算法加密傳輸對象可以保護數據。這就是所謂的對對象進行密封。而對密封的對象進行數字簽名則可以防止對象被非法篡改,保持其完整性。在以下場景中,需要對對象密封和數字簽名來保證數據安全:
- 序列化或傳輸敏感數據
- 沒有諸如SSL傳輸通道一類的安全通信通道或者對於有限的事務來說代價太高
- 敏感數據需要長久保存(比如在硬盤驅動器上)
應該避免使用私有加密算法。這類算法大多數情況下會引入不必要的漏洞。
示例:
demo class
/**
* 測試 map demo class
*/
public class SerializableMap<K, V> implements Serializable {
final static long serialVersionUID = 45217497203262395L;
private Map<K, V> map;
public SerializableMap()
{
map = new HashMap<K, V>();
}
public V getData(K key)
{
return map.get(key);
}
public void setData(K key, V data)
{
map.put(key, data);
}
}
/**
* 構造map數據
*
*/
public class MapBuilder {
public static SerializableMap<String, Integer> buildMap() {
SerializableMap<String, Integer> map = new SerializableMap<String, Integer>();
map.setData("John Doe", new Integer(123456789));
map.setData("Richard Roe", new Integer(246813579));
return map;
}
public static void InspectMap(SerializableMap<String, Integer> map) {
System.out.println("John Doe's number is " + map.getData("John Doe"));
System.out.println("Richard Roe's number is "
+ map.getData("Richard Roe"));
}
}
開始序列化以及反序列化
public class TestSerializer {
// 加密器
private static Cipher cipher;
// 加密 解密 key
private static SecretKey key;
// 簽名 key
private static KeyPair kp;
// 簽名
private static Signature sig;
public static void main(String[] args) throws Exception {
// 僅加密
encryption();
// 先加密后簽名,會偽造簽名惡意攻擊產生
// 先簽名,后加密
encryptionAfterSig();
}
/**
* 只加密,無法進行可靠性驗證
*
* @throws Exception
*/
private static void encryption() throws Exception {
// Build map
SerializableMap<String, Integer> map = buildMap();
SealedObject sealedMap = encrypt(map);
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
// Unseal map
map = (SerializableMap<String, Integer>) decrypt(sealedMap);
// Inspect map
InspectMap(map);
}
/**
* 先簽名 后加密
*/
private static void encryptionAfterSig() throws Exception {
// Build map
SerializableMap<String, Integer> map = buildMap();
// sig
SignedObject signedMap = sigObject(map);
// encrypt
SealedObject sealedMap = encrypt(signedMap);
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
signedMap = (SignedObject) decrypt(sealedMap);
// Verify signature and retrieve map
if (!signedMap.verify(kp.getPublic(), sig)) {
throw new GeneralSecurityException("Map failed verification");
}
map = (SerializableMap<String, Integer>) signedMap.getObject();
// Inspect map
InspectMap(map);
}
/**
* 簽名
*
* @param map
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws IOException
* @throws SignatureException
*/
private static SignedObject sigObject(Serializable map) throws NoSuchAlgorithmException, InvalidKeyException, IOException, SignatureException {
// Generate signing public/private key pair & sign map
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kp = kpg.generateKeyPair();
sig = Signature.getInstance("SHA256withRSA");
return new SignedObject(map, kp.getPrivate(), sig);
}
/**
* 加密
*
* @param obj
* @return
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws IOException
* @throws IllegalBlockSizeException
* @throws InvalidKeyException
*/
private static SealedObject encrypt(Serializable obj) throws NoSuchAlgorithmException, NoSuchPaddingException, IOException, IllegalBlockSizeException, InvalidKeyException {
// password
String password = "this is an encrypted key";
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(128, new SecureRandom(password.getBytes(Charset.defaultCharset())));
key = generator.generateKey();
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return new SealedObject(obj, cipher);
}
/**
* 解密
*
* @param sealedMap
* @return
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws ClassNotFoundException
* @throws IOException
*/
private static Object decrypt(SealedObject sealedMap) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, ClassNotFoundException, IOException, BadPaddingException, IllegalBlockSizeException {
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
return sealedMap.getObject(cipher);
}
禁止序列化未加密的敏感數據
雖然序列化可以將對象的狀態保存為一個字節序列,之后通過反序列化該字節序列又能重新構造出原來的對象,但是它並沒有提供一種機制來保證序列化數據的安全性。可訪問序列化數據的攻擊者可以借此獲取敏感信息並確定對象的實現細節。攻擊者也可惡意修改其中的數據,試圖在其被反序列化之后對系統造成危害。因此,敏感數據序列化之后是潛在對外暴露着的。永遠不應該被序列化的敏感信息包括:密鑰、數字證書、以及那些在序列化時引用敏感數據的類。此條規則的意義在於防止敏感數據被無意識的序列化導致敏感信息泄露。
在將某個包含敏感數據的類序列化時,程序必須確保敏感數據不被序列化。這包括阻止包含敏感信息的數據成員被序列化,以及不可序列化或者敏感對象的引用被序列化。該示例將相關字段聲明為transient,從而使它們不包括在依照默認的序列化機制應該被序列化的字段列表中。這樣既避免了錯誤的序列化,又防止了敏感數據被意外序列化。
通過定義serialPersistentFields數組字段來確保敏感字段被排除在序列化之外,除了上述方案,也可以通過自定義writeObject()、writeReplace()、writeExternal()這些函數,不將包含敏感信息的字段寫到序列化字節流中。
public class GPSLocation implements Serializable {
private double x;
private double y;
private String id;
// 敏感字段x,y不在序列化字段數組 serialPersistentFields中,將不會被序列化
private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("id", String.class)};
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String toString() {
return "GPSLocation{" +
"x=" + x +
", y=" + y +
", id='" + id + '\'' +
'}';
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
GPSLocation gps = new GPSLocation();
gps.setX(72.0);
gps.setY(118.22);
gps.setId("id");
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(gps);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
gps = (GPSLocation) in.readObject();
in.close();
System.out.println(gps);
}
}
防止序列化和反序列化被利用來繞過安全管理
序列化和反序列化可能被利用來繞過安全管理器的檢查。一個可序列化類的構造器中出於防止不可信代碼修改類的內部狀態等原因可能需要引入安全管理器的檢查。這種安全管理器的檢查必須應用到所有能夠構建類實例的地方。例如,如果某個類依據安全檢查的結果來判定調用者是否能夠讀取其敏感內部狀態,那么這類安全檢查必須也在反序列化中應用。這就確保了攻擊者無法通過反序列化對象來提取敏感信息。
// 錯誤示例
public final class Hometown implements Serializable
{
private static final long serialVersionUID = 9078808681344666097L;
// Private internal state
private String town;
private static final String UNKNOWN = "UNKNOWN";
void performSecurityManagerCheck() throws SecurityException
{
// verify whether current user has rights to access the file
}
void validateInput(String newCC) throws InvalidInputException
{
// ...
}
public Hometown()
{
performSecurityManagerCheck();
// Initialize town to default value
town = UNKNOWN;
}
// Allows callers to retrieve internal state
String getValue()
{
performSecurityManagerCheck();
return town;
}
// Allows callers to modify (private) internal state
public void changeTown(String newTown) throws InvalidInputException
{
if (town.equals(newTown))
{
// No change
return;
}
else
{
performSecurityManagerCheck();
validateInput(newTown);
town = newTown;
}
}
private void writeObject(ObjectOutputStream out) throws IOException
{
out.writeObject(town);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
in.defaultReadObject();
// If the deserialized name does not match
// the default value normally
// created at construction time, duplicate the checks
if (!UNKNOWN.equals(town))
{
validateInput(town);
}
}
}
錯誤示例中,安全管理器檢查被應用在構造器中,但在序列化與反序列化涉及的writeObject()和readObject()方法中沒有用到。這樣會允許非信任代碼惡意創建類實例。
正確示例
public final class Hometown implements Serializable
{
// ... all methods the same except the following:
// writeObject() correctly enforces checks during serialization
private void writeObject(ObjectOutputStream out) throws IOException
{
performSecurityManagerCheck();
out.writeObject(town);
}
// readObject() correctly enforces checks during deserialization
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
in.defaultReadObject();
// If the deserialized name does not match the default value normally
// created at construction time, duplicate the checks
if (!UNKNOWN.equals(town))
{
performSecurityManagerCheck();
validateInput(town);
}
}
}