Java 开发 2.0: 保护用于云计算的 Java 应用程序数据

使用私钥加密来保护云数据

数据安全是考虑采用云计算的组织面临的一个严峻问题,但在许多情况下,无需对它过于担忧。在本期 Java 开发 2.0 中,将学习如何使用私钥加密和高级加密标准来保护用于云的敏感的应用程序数据。您还将快速了解加密策略,这对最大限度地提高针对分布式云数据存储的条件式搜索效率至关重要。

Andrew Glover, CTO, App47

/developerworks/i/p-anglover.jpgAndrew Glover 是一位具有行为驱动开发、持续集成和敏捷软件开发激情的开发人员、作家、演说家和企业家。他是 easyb 行为驱动开发 (Behavior-Driven Development, BDD) 框架的创建者和以下三本书的合著者:持续集成Groovy 在行动Java 测试模式



2012 年 3 月 05 日

关于本系列

自 Java 技术首次诞生以来,Java 开发格局已发生了翻天覆地的变化。得益于成熟的开源框架和可靠的租赁部署基础架构,现在可以迅速而经济地组装、测试、运行和维护 Java 应用程序。在本 系列 文章中,Andrew Glover 将探索使得新的 Java 开发风格成为可能的各种技术和工具。

短短几年,云计算平台和服务就大大改变了 Java™ 应用程序开发领域。它们降低了与系统维护和配置关联的门槛,同时降低了将软件推向市场的成本 并提高了上市速度。从概念上讲,云计算具有重大意义:业务经理喜欢它的投资回报,开发人员喜欢摆脱基础架构代码的自由感。但是,许多商店仍然在纠结是否迁移到云平台。

数据安全是考虑将其软件迁移到云基础架构的组织面临的主要问题之一。数据越敏感,担忧的理由就越多。作为软件开发人员,我们一定要理解云计算的真实安全风险和解决至少部分问题的真实方法。

在本期 Java 开发 2.0 中,我将解释将数据存储在云中与将它存储在集中化的机器中有何不同。我然后将介绍如何使用 Java 平台内置的私钥加密标准和实用程序,合理地保护您的数据,即使这些数据存储在分布式云数据存储中。最后,我将演示一种战略性的加密方法,使用查询条件作为是否加密您的数据的判断标准。

保护云数据

云计算实际上并没有引入新的数据安全问题;在某些情形下,它只是放大了这些问题。将数据放在云中可能会将这些数据暴露给更大的群体,这通常是一件好事。但是,如果暴露的数据本应是私有的,或者只能有条件地访问,那么结果可能是灾难性的。云计算的基本问题是,它使委托的数据不再受开发人员或系统管理员直接掌控。没有在本地存储和管理,云中的数据存储在分布式设备上,这些设备可能位于任何地方,任何人都可以访问它们。

欧盟中的数据隐私

欧盟的云平台数据隐私政策比美国的政策严格得多:属于欧盟居民的个人数据(比如一位法国居民的医疗记录)必须位于存在于欧盟内部的服务器上。

即使您的公司能够适应分散化、遥远的数据存储的事实,您可能仍然希望您放置在云中的应用程序存在一点点的数据安全性。当您开始考虑数据安全时,就会出现两个重要的问题:

  • 数据在传输期间是否安全?
  • 数据在静止时是否安全?

传输中的数据 关系到数据如何从一个位置传输到另一个位置;也就是说,您使用了何种通信技术和基础架构。静止的数据 关系到如何(和多好地)存储数据。例如,如果您在数据库中存储用户名和密码,而不加密它们,那么您的静止数据就会不安全。

要保护在网络中传输的数据,通常需要使用 HTTPS。这是 HTTP 与浏览器与客户端之间的数据加密的结合。HTTPS 的另一项优势是流行性:大部分开发人员都已将 Apache、Tomcat 或 Jetty 配置为使用 HTTPS。

加密也是保护静止数据的一种常见机制,云计算没有改变这一事实。尽管加密的原理可能很复杂,但您只需要知道一些有关它的基本知识,就可以合理地保护您的应用程序数据。而且,一旦您的数据是安全的,那么您是在本地提供它还是通过云平台或数据库提供它就变得不那么重要。


私钥加密

加密是将人类可读的明文转换为不可读的文本的过程。您可以使用一个加密算法(也称为一个密码)来实现加密。加密的文本会通过一个密钥将文本解密成可读的文本,这个密钥实际上是一个密码。加密通过使信息无法供任何没有密钥的人读取来保护信息。

计算机领域中使用了两种类型的密钥加密:公钥加密和私钥加密。公钥加密 是保护传输中的数据的最常见技术;事实上,它是 HTTPS 事务安全性的底层架构。这种形式的加密需要一个公私钥集合 中的两个密钥。公钥用于加密数据,而私钥用于解密相应的数据。在公钥加密中,公钥可安全地分发,而私钥必须由管理员控制。公钥加密简化了已加密信息的共享。

私钥和隐私

无论您使用何种加密算法,您必须确保私钥是安全的。您的密码应该满足高安全标准,绝不应以明文形式存储 — 尤其是不要存储在云中。幸运的是,Java 平台的安全基础架构创造适当复杂的密钥,允许您在 Java 平台密钥库中保护它们。

私钥加密 中,数据使用一个私钥加密和解密。这种加密类型使得与第三方共享加密的数据变得很困难,因为发送方和接收方都必须使用相同的密钥。如果密钥被破坏,那么所有加密的信息就会被破坏。如果加密的数据无需与其他方共享,密钥可以始终受到严格控制,那么私钥加密非常有效。

私钥加密是保护将通过云基础架构存储和传输的应用程序数据的一种有效方式。因为加密密钥保持在管理员或应用程序创建者的控制之下,云提供者和其他潜在的窃听者无法不受控制地访问该数据。


加密 Java 应用程序

您可以选择各种选项来保护 Java 应用程序,包括标准 Java 平台库。您也可以从众多加密标准和包中进行选择。对于下面的示例,我将使用核心 Java 库和 AES(高级加密标准)。我将使用一个私钥来加密明文和解密密文,也就是加密之后的明文。我喜欢 AES,因为它是经过美国国家安全署批准的,还经过了美国政府的标准化。

为了实现最高的灵活性和测试的简单性,我将创建一些密码接口以及一些只是包装核心 Java 类的相关联的实现类。我然后将介绍如何使用这些类来实现安全的持久保存,以及如何查询云数据存储(比如 Amazon 的 SimpleDB 或者甚至 MongoHQ 的 MongoDB)中的数据。

在清单 1 中,我定义了一个简单的通用加密接口,它为加密和解密数据定义了两种方法。该接口将用作各种算法的前端,也就是说,我的实现类将使用一种特定的密码算法,比如 AES。

清单 1. 一种加密接口
package com.b50.crypto;

public interface Cryptographical {
 String encrypt(String plaintext);
 String decrypt(String ciphertext);
}

使用我的 Cryptographical 接口,我可以加密文本或解密已加密的文本。接下来,在清单 2 中,我将使用 Java 安全性 API 来创建另一个表示密钥的接口:

清单 2. 一种密钥接口
package com.b50.crypto;

import java.security.Key;

public interface CryptoKeyable {
  Key getKey();
}

从上述代码中可以看到,我的 CryptoKeyable 接口仅仅用作 Java 平台的核心 Key 类型的包装器。

如果您使用 AES 加密,那么在加密明文时生成的二进制字符时,会需要使用 base-64 编码,至少在您希望在 Web 请求(例如,使用 SimpleDB 域)中使用它们时需要使用 base-64 编码。因此,我将编码所有加密的字符串,解码任何已加密的字符串。

用于 AES 的 Cryptographical 实现类(如清单 3 所示)不仅要处理 AES 加密,还要处理 base-64 编码和解码:

清单 3. 我的 Cryptographical 接口的一种 AES 实现
package com.b50.crypto;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;

public class AESCryptoImpl implements Cryptographical {

 private Key key;
 private Cipher ecipher;
 private Cipher dcipher;

 private AESCryptoImpl(Key key) throws NoSuchAlgorithmException,
   NoSuchPaddingException, InvalidKeyException {
  this.key = key;
  this.ecipher = Cipher.getInstance("AES");
  this.dcipher = Cipher.getInstance("AES");
  this.ecipher.init(Cipher.ENCRYPT_MODE, key);
  this.dcipher.init(Cipher.DECRYPT_MODE, key);
 }

 public static Cryptographical initialize(CryptoKeyable key) throws CryptoException {
  try {
   return new AESCryptoImpl(key.getKey());
  } catch (NoSuchAlgorithmException e) {
   throw new CryptoException(e);
  } catch (NoSuchPaddingException e) {
   throw new CryptoException(e);
  } catch (InvalidKeyException e) {
   throw new CryptoException(e);
  }
 }

 public String encrypt(String plaintext) {
  try {
   return new BASE64Encoder().encode(ecipher.doFinal(plaintext.getBytes("UTF8")));
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
 }

 public String decrypt(String ciphertext) {
  try {
   return new String(dcipher.doFinal(new BASE64Decoder().decodeBuffer(ciphertext)), 
     "UTF8");
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
 }
}

Java 密钥库

接下来,让我们想想加密密钥。Java 平台的核心库可用于创建强加密密钥;但是,这些方法总是会得到一个随机生成的新密钥。所以,如果您使用 Java KeyGenerator 类创建密钥,则需要存储该密钥,以供将来使用(也就是说,在您决定使用该密钥解密已加密的文本之前使用)。为此,您可以使用 Java 平台 KeyStore 实用程序和相应的类。

KeyStore 包含一组类,这些类使您能够将密钥保存到采用密码保护的二进制文件(名为 keystore)中。我可以使用一些测试案例来测试 Java 中的密钥。首先,我创建一个 Key 的两个实例,显示每个实例对应的加密的 String 是不同的,如清单 4 所示:

清单 4. 使用两个不同密钥的简单加密
@Test
public void testEncryptRandomKey() throws Exception {
 SecretKey key = KeyGenerator.getInstance("AES").generateKey();
 Cryptographical crypto = AESCryptoImpl.initialize(new AESCryptoKey(key));
 String enc = crypto.encrypt("Andy");
 Assert.assertEquals("Andy", crypto.decrypt(enc));

 SecretKey anotherKey = KeyGenerator.getInstance("AES").generateKey();
 Cryptographical anotherInst = AESCryptoImpl.initialize(new AESCryptoKey(anotherKey));
 String anotherEncrypt = anotherInst.encrypt("Andy");
 Assert.assertEquals("Andy", anotherInst.decrypt(anotherEncrypt));

 Assert.assertFalse(anotherEncrypt.equals(enc));
}

接下来,在清单 5 中,我证明了一个给定的密钥实例始终会得到相应 String 的相同加密文本:

清单 5. 对应于一个字符串的一个私钥
@Test
public void testEncrypt() throws Exception {
 SecretKey key = KeyGenerator.getInstance("AES").generateKey();

 KeyStore ks = KeyStore.getInstance("JCEKS");
 ks.load(null, null);
 KeyStore.SecretKeyEntry skEntry = new KeyStore.SecretKeyEntry(key);
 ks.setEntry("mykey", skEntry, 
   new KeyStore.PasswordProtection("mykeypassword".toCharArray()));
 FileOutputStream fos = new FileOutputStream("agb50.keystore");
 ks.store(fos, "somepassword".toCharArray());
 fos.close();

 Cryptographical crypto = AESCryptoImpl.initialize(new AESCryptoKey(key));
 String enc = crypto.encrypt("Andy");
 Assert.assertEquals("Andy", crypto.decrypt(enc));

 //alternatively, read the keystore file itself to obtain the key

 Cryptographical anotherInst = AESCryptoImpl.initialize(new AESCryptoKey(key));
 String anotherEncrypt = anotherInst.encrypt("Andy");
 Assert.assertEquals("Andy", anotherInst.decrypt(anotherEncrypt));

 Assert.assertTrue(anotherEncrypt.equals(enc));
}

我使用了特定密钥加密的内容,因此需要使用相同密钥来解密。使用 Java KeyStore 是一种既方便又安全的密钥存储方法。

密钥库相关说明

清单 4 和清单 5 中是样例代码,但它可以演示许多事情:

  • 密钥库有一个名称。
  • 存储的文件受密码保护。
  • 密钥库可存储多于一个密钥。
  • 密钥库中的每个密钥都有一个关联的密码。

对于这个测试案例,我决定在每次运行测试时创建一个新密钥库。我可以轻松地为每个新测试打开一个现有密钥库。如果我希望使用一个现有的密钥库,则需要知道它的密码,以及访问特定密钥的密码。

对于加密,密钥就是一切。底层密码有多强并不重要;如果我的密钥被破坏,那么我的数据就会暴露。这也意味着需要确保密钥库及其关联的密码始终都是安全的。(例如,在生产环境的应用程序中,我不会像清单 4 和清单 5 中演示的那样硬编码密码。)


云加密

当您加密数据时,您会更改它的属性。基本上,这意味着一个加密的整数不会很好地响应整数对比。因此,可以思考一下您最终会如何、为什么和在何种情形下查询存储在云中的数据,这很重要。好消息是,在许多情形下,您希望保持私有的数据将与您希望操作的数据具有不同的业务价值:加密一个帐户的名称或与帐户所有者相关的某些个人信息是合理的,但加密帐户余额可能没有意义(因为没有人会关心无法关联到一个人的帐户余额)。

查询加密数据

在执行准确 匹配时,加密的数据很容易搜索,比如:“为我找到所有名为 ‘foo’ 的帐户(其中 ‘foo’ 是加密的)。”但是这对于如下所示的条件查询没有效:“为我找到其到期余额大于 450 美元(其中 450 美元是加密的)的所有帐户。”

例如,让我们想象一下,我使用一个简单的密码,它将一个字符串的字符倒序排列并在其末尾添加一个字符 i。在此情况下,字符串 foo 将变成 oofi450 将变成 054i。如果表中的名称值是使用这个简单密码加密的,我可以按准确匹配来轻松地查询,比如 “select * from table where name = 'oofi'”。但相比而言,450 的加密值完全不同:“select * from table where amount > 054i” 与 “select * from table where amount > 450” 不太一样。

为了在此情形下进行数据比较,我可能必须在应用程序中执行某种解密,也就是说,我需要选择表中所包含的某个数据来解密 amount 字段,然后执行比较。对于此行为,无法依赖于底层数据存储意味着,我的过滤操作可能不会像在数据存储中那么快。假设我希望最大化效率,我应该考虑我希望加密何种数据,以及我希望如何加密它。加密时考虑未来的查询是一种改善程序总体效率的不错方式。

您可以轻松地加密 MongoDB 中的一个帐户名,并按它加密的名称进行搜索,如清单 6 所示:

清单 6. 加密 MongoDB 中的数据
@Test
public void encryptMongoDBRecords() throws Exception {
 KeyStore.SecretKeyEntry pkEntry = getKeyStoreEntry();
 Cryptographical crypto = 
   AESCryptoImpl.initialize(new AESCryptoKey(pkEntry.getSecretKey()));

 DB db = getMongoConnection();
 DBCollection coll = db.getCollection("accounts");

 BasicDBObject encryptedDoc = new BasicDBObject();
 encryptedDoc.put("name", crypto.encrypt("Acme Life, LLC"));
 coll.insert(encryptedDoc);


 BasicDBObject encryptedQuery = new BasicDBObject();
 encryptedQuery.put("name", crypto.encrypt("Acme Life, LLC"));

 DBObject result = coll.findOne(encryptedQuery);
 String value = result.get("name").toString();
 Assert.assertEquals("Acme Life, LLC", crypto.decrypt(value));
}

我在 清单 6 中所做的第一步是,使用 getKeyStoreEntry 方法读取一个现有的密钥库。接下来获取一个 MongoDB 实例的连接,在本例中,它恰好位于 MongoHQ 上的云中。然后我会获取帐户集合(RDBMS 程序员会称之为帐户表)的链接,继续插入一个新帐户记录,其相应的名称已加密。最后,我会通过加密我的搜索字符串,搜索相同的记录(其中 name 等于加密的 “Acme Life, LLC”)。

MongoDB 中的记录看起来将类似清单 7 中所示的内容。(请注意,您加密的 “Acme Life, LLC” 字符串将与我的不同,因为您将使用不同的密钥。)

清单 7. 一个 MongoDB 加密测试案例
{
 _id : "4ee0c541300484530bf9c6fa",
 name : "f0wJxYyVhfH0UkkTLKGZng=="
}

我在文档中保持实际密钥 (name) 不加密,但我也可以加密它。如果我这么做,我相应的查询将需要反映这一变化。我也会加密集合名称。直接 String 对比将生效,无论它们是否加密。

这种策略不受限于 MongoDB 实现。例如,我可以对 SimpleDB 执行大体相同的测试案例,如清单 8 所示:

清单 8. 一个 SimpleDB 加密测试案例
@Test
public void testSimpleDBEncryptInsert() throws Exception {

 KeyStore.SecretKeyEntry pkEntry = getKeyStoreEntry();
 Cryptographical crypto = 
   AESCryptoImpl.initialize(new AESCryptoKey(pkEntry.getSecretKey()));

 AmazonSimpleDB sdb = getSimpleDB();
 String domain = "accounts";
 sdb.createDomain(new CreateDomainRequest(domain));

 List<ReplaceableItem> data = new ArrayList<ReplaceableItem>();

 String encryptedName = crypto.encrypt("Acme Life, LLC");

 data.add(new ReplaceableItem().withName("account_02").withAttributes(
  new ReplaceableAttribute().withName("name").withValue(encryptedName)));

 sdb.batchPutAttributes(new BatchPutAttributesRequest(domain, data));

 String qry = "select * from " + SimpleDBUtils.quoteName(domain) 
   + " where name = '" + encryptedName + "'";

 SelectRequest selectRequest = new SelectRequest(qry);
 for (Item item : sdb.select(selectRequest).getItems()) {
  Assert.assertEquals("account_02", item.getName());
 }
}

这里,我采用了与 MongoDB 示例相同的步骤:我从一个现有的密钥库读取数据,获取 Amazon 的 SimpleDB 的连接,然后插入一个帐户记录,该记录的 name 属性已加密。最后,我按名称插糟帐户,使用加密的值作为它的密钥。


结束语

尽管云计算有望让您的数据可供广大群体访问,但您也可以通过许多方法来保护敏感数据。在本文中,我介绍了如何使用 Java 平台库来保护云基础架构(比如 MongoDB 或 SimpleDB)上静止的数据。私钥加密为数据管理员提供了数据安全保护。将私钥存储在您的 Java KeyStore 中,会使它们可管理且安全。这是访问私钥的惟一密码,您绝对不希望在靠近云的任何地方以明文形式存储该密码。

在一些搜索上下文中,存储的已加密数据的行为方式与明文数据不同。准确匹配非常有用,而涉及非准确匹配的条件查询可能会令人感到头疼。这里的解决方案取决于您如何处理(或者不处理)对比操作。在考虑您将加密何种内容时,始终需要考虑到您想要的查询。加密所有数据可能有些小题大作,所以一定要考虑查询的内容和方式。

参考资料

学习

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、群和维基,并联系其他 developerWorks 用户。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Cloud computing
ArticleID=800523
ArticleTitle=Java 开发 2.0: 保护用于云计算的 Java 应用程序数据
publish-date=03052012