假设我正在尝试从使用基本身份验证/基本证书的RESTful api中提取,那么在我的程序中存储该用户名和密码的最佳方法是什么?现在它只是以明文坐在那里。
UsernamePasswordCredentials creds = new UsernamePasswordCredentials("myName@myserver","myPassword1234");
有没有一种方法可以做到这一点更安全?
由于
答案 0 :(得分:92)
有了内心到外在的心态,这里有一些保护你的过程的步骤:
首先,您应该将密码处理从String
更改为character array
。
原因是String
是immutable
对象,因此即使对象设置为null
,它的数据也不会立即被清除;数据设置为垃圾收集,这会带来安全问题,因为恶意程序在清理之前可能会访问String
(密码)数据。
这是弃用Swing's JPasswordField's getText()
方法的主要原因,以及为什么getPassword()
uses character arrays。
第二步是加密您的凭据,只在身份验证过程中临时解密。
这与第一步类似,确保您的漏洞时间尽可能小。
建议您的凭据不是硬编码的,而是以集中,可配置且易于维护的方式存储,例如配置或属性文件。
您应该在保存文件之前加密凭据,此外,您可以对文件本身应用第二次加密(对凭据进行2层加密,对其他文件内容进行1层加密)。
请注意,上面提到的两个加密过程中的每一个都可以是多层的。作为概念性示例,每个加密可以是Triple Data Encryption Standard (AKA TDES and 3DES)的单独应用。
在您的本地环境得到适当保护后(但请记住,它永远不会“安全”!),第三步是使用TLS (Transport Layer Security) or SSL (Secure Sockets Layer)对您的传输过程应用基本保护。
第四步是采用其他保护方法。
例如,将混淆技术应用于“使用中”编译,以避免(即使很快)暴露您的安全措施,以防您的程序由Ms. Eve, Mr. Mallory, or someone else (the bad-guys)获得并反编译。
更新1:
根据@ Damien.Bell的要求,这是一个涵盖第一步和第二步的例子:
//These will be used as the source of the configuration file's stored attributes.
private static final Map<String, String> COMMON_ATTRIBUTES = new HashMap<String, String>();
private static final Map<String, char[]> SECURE_ATTRIBUTES = new HashMap<String, char[]>();
//Ciphering (encryption and decryption) password/key.
private static final char[] PASSWORD = "Unauthorized_Personel_Is_Unauthorized".toCharArray();
//Cipher salt.
private static final byte[] SALT = {
(byte) 0xde, (byte) 0x33, (byte) 0x10, (byte) 0x12,
(byte) 0xde, (byte) 0x33, (byte) 0x10, (byte) 0x12,};
//Desktop dir:
private static final File DESKTOP = new File(System.getProperty("user.home") + "/Desktop");
//File names:
private static final String NO_ENCRYPTION = "no_layers.txt";
private static final String SINGLE_LAYER = "single_layer.txt";
private static final String DOUBLE_LAYER = "double_layer.txt";
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws GeneralSecurityException, FileNotFoundException, IOException {
//Set common attributes.
COMMON_ATTRIBUTES.put("Gender", "Male");
COMMON_ATTRIBUTES.put("Age", "21");
COMMON_ATTRIBUTES.put("Name", "Hypot Hetical");
COMMON_ATTRIBUTES.put("Nickname", "HH");
/*
* Set secure attributes.
* NOTE: Ignore the use of Strings here, it's being used for convenience only.
* In real implementations, JPasswordField.getPassword() would send the arrays directly.
*/
SECURE_ATTRIBUTES.put("Username", "Hypothetical".toCharArray());
SECURE_ATTRIBUTES.put("Password", "LetMePass_Word".toCharArray());
/*
* For demosntration purposes, I make the three encryption layer-levels I mention.
* To leave no doubt the code works, I use real file IO.
*/
//File without encryption.
create_EncryptedFile(NO_ENCRYPTION, COMMON_ATTRIBUTES, SECURE_ATTRIBUTES, 0);
//File with encryption to secure attributes only.
create_EncryptedFile(SINGLE_LAYER, COMMON_ATTRIBUTES, SECURE_ATTRIBUTES, 1);
//File completely encrypted, including re-encryption of secure attributes.
create_EncryptedFile(DOUBLE_LAYER, COMMON_ATTRIBUTES, SECURE_ATTRIBUTES, 2);
/*
* Show contents of all three encryption levels, from file.
*/
System.out.println("NO ENCRYPTION: \n" + readFile_NoDecryption(NO_ENCRYPTION) + "\n\n\n");
System.out.println("SINGLE LAYER ENCRYPTION: \n" + readFile_NoDecryption(SINGLE_LAYER) + "\n\n\n");
System.out.println("DOUBLE LAYER ENCRYPTION: \n" + readFile_NoDecryption(DOUBLE_LAYER) + "\n\n\n");
/*
* Decryption is demonstrated with the Double-Layer encryption file.
*/
//Descrypt first layer. (file content) (REMEMBER: Layers are in reverse order from writing).
String decryptedContent = readFile_ApplyDecryption(DOUBLE_LAYER);
System.out.println("READ: [first layer decrypted]\n" + decryptedContent + "\n\n\n");
//Decrypt second layer (secure data).
for (String line : decryptedContent.split("\n")) {
String[] pair = line.split(": ", 2);
if (pair[0].equalsIgnoreCase("Username") || pair[0].equalsIgnoreCase("Password")) {
System.out.println("Decrypted: " + pair[0] + ": " + decrypt(pair[1]));
}
}
}
private static String encrypt(byte[] property) throws GeneralSecurityException {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(PASSWORD));
Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
pbeCipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(SALT, 20));
//Encrypt and save to temporary storage.
String encrypted = Base64.encodeBytes(pbeCipher.doFinal(property));
//Cleanup data-sources - Leave no traces behind.
for (int i = 0; i < property.length; i++) {
property[i] = 0;
}
property = null;
System.gc();
//Return encryption result.
return encrypted;
}
private static String encrypt(char[] property) throws GeneralSecurityException {
//Prepare and encrypt.
byte[] bytes = new byte[property.length];
for (int i = 0; i < property.length; i++) {
bytes[i] = (byte) property[i];
}
String encrypted = encrypt(bytes);
/*
* Cleanup property here. (child data-source 'bytes' is cleaned inside 'encrypt(byte[])').
* It's not being done because the sources are being used multiple times for the different layer samples.
*/
// for (int i = 0; i < property.length; i++) { //cleanup allocated data.
// property[i] = 0;
// }
// property = null; //de-allocate data (set for GC).
// System.gc(); //Attempt triggering garbage-collection.
return encrypted;
}
private static String encrypt(String property) throws GeneralSecurityException {
String encrypted = encrypt(property.getBytes());
/*
* Strings can't really have their allocated data cleaned before CG,
* that's why secure data should be handled with char[] or byte[].
* Still, don't forget to set for GC, even for data of sesser importancy;
* You are making everything safer still, and freeing up memory as bonus.
*/
property = null;
return encrypted;
}
private static String decrypt(String property) throws GeneralSecurityException, IOException {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(PASSWORD));
Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
pbeCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(SALT, 20));
return new String(pbeCipher.doFinal(Base64.decode(property)));
}
private static void create_EncryptedFile(
String fileName,
Map<String, String> commonAttributes,
Map<String, char[]> secureAttributes,
int layers)
throws GeneralSecurityException, FileNotFoundException, IOException {
StringBuilder sb = new StringBuilder();
for (String k : commonAttributes.keySet()) {
sb.append(k).append(": ").append(commonAttributes.get(k)).append(System.lineSeparator());
}
//First encryption layer. Encrypts secure attribute values only.
for (String k : secureAttributes.keySet()) {
String encryptedValue;
if (layers >= 1) {
encryptedValue = encrypt(secureAttributes.get(k));
} else {
encryptedValue = new String(secureAttributes.get(k));
}
sb.append(k).append(": ").append(encryptedValue).append(System.lineSeparator());
}
//Prepare file and file-writing process.
File f = new File(DESKTOP, fileName);
if (!f.getParentFile().exists()) {
f.getParentFile().mkdirs();
} else if (f.exists()) {
f.delete();
}
BufferedWriter bw = new BufferedWriter(new FileWriter(f));
//Second encryption layer. Encrypts whole file content including previously encrypted stuff.
if (layers >= 2) {
bw.append(encrypt(sb.toString().trim()));
} else {
bw.append(sb.toString().trim());
}
bw.flush();
bw.close();
}
private static String readFile_NoDecryption(String fileName) throws FileNotFoundException, IOException, GeneralSecurityException {
File f = new File(DESKTOP, fileName);
BufferedReader br = new BufferedReader(new FileReader(f));
StringBuilder sb = new StringBuilder();
while (br.ready()) {
sb.append(br.readLine()).append(System.lineSeparator());
}
return sb.toString();
}
private static String readFile_ApplyDecryption(String fileName) throws FileNotFoundException, IOException, GeneralSecurityException {
File f = new File(DESKTOP, fileName);
BufferedReader br = new BufferedReader(new FileReader(f));
StringBuilder sb = new StringBuilder();
while (br.ready()) {
sb.append(br.readLine()).append(System.lineSeparator());
}
return decrypt(sb.toString());
}
一个完整的例子,解决每个保护步骤,远远超出我认为对这个问题合理的范围,因为它是关于“步骤是什么”,而不是“如何应用它们“
这将远远超过我的答案(最后的抽样),而其他问题在S.O.已经针对这些步骤的“如何”,更合适,并且对每个步骤的实施提供了更好的解释和抽样。
答案 1 :(得分:7)
如果您使用的是基本身份验证,则应将其与SSL结合使用,以避免在base64编码的纯文本中传递凭据。您不希望让嗅探数据包的人轻松获取您的凭据。此外,请勿在源代码中对您的凭据进行硬编码。使它们可配置。从配置文件中读取它们。您应该在将凭据存储到配置文件之前加密凭据,并且一旦从配置文件中读取凭据,您的应用就应该解密凭证。
答案 2 :(得分:2)
加上我忘了的一千件事情:)
答案 3 :(得分:1)
加密凭据通常不是一个好建议。加密的东西可以解密。常见的最佳做法是将密码存储为salted hash。无法解密哈希。添加盐以击败使用Rainbow Tables的暴力猜测。只要每个userId都有自己的随机盐,攻击者就必须为盐的每个可能值生成一组表,快速在宇宙的生命周期内使这种攻击成为不可能。这就是为什么网站一旦忘记密码就无法向您发送密码的原因,但他们只能“重置”密码。他们没有存储您的密码,只有它的哈希值。
密码散列本身并不是很难实现,但解决这个问题是无数其他人已经为你完成的。我发现jBcrypt易于使用。
作为防止强力猜测密码的额外保护,通常最佳做法是在使用错误密码进行一定次数的登录尝试后强制userId或远程IP等待几秒钟。如果没有这个,蛮力攻击者可以猜出服务器可以处理的每秒密码数。能够每10秒钟或100万次猜测100个密码之间存在巨大差异。
我认为您在源代码中包含了用户名/密码组合。这意味着如果您想要更改密码,则必须重新编译,停止并重新启动您的服务,这也意味着任何获取源代码的人也会拥有您的密码。常见的最佳做法是永远不要这样做,而是在您的数据存储区中存储凭据(用户名,密码哈希,密码盐)
答案 4 :(得分:0)
人们为什么谈论哈希。 OP希望存储其用户凭据以访问外部资源。散列密码无济于事。
现在,这已成为现实。我只是为每个图层提供简单的最佳做法。
1。将密码存储在Java应用程序中。 :将其存储为Char Array。创建一个密码存储类,并将密码存储为hashmap,其中key为您要访问的资源,并将其作为包含用户名和密码的某个对象的值。使用某些身份验证将入口点限制为该api,例如:接受登录用户的凭据以验证该用户对该资源的访问级别(只需将用户映射到他们可以访问的密码列表即可。如果有很多,请创建一个组并将密码映射密钥映射到该组)。除此以外,存储密码的一切取决于您对jvm本身有多大的疑惑来泄漏它。
答案 5 :(得分:0)
避免在源代码中存储凭据通常是一个好主意。 问题是,对代码的访问以及谁应该有权访问凭据通常会随着时间的推移而发生变化。一旦项目变得更加成熟,通常会有一些开发人员不需要知道,因此不应该知道某些凭据。此外,代码可能会被重用用于稍微不同的目的,甚至成为开源。此外,随着代码库变得越来越复杂,识别隐藏在代码中间某处的凭据变得非常乏味。
可以肯定地说,数亿用户已经受到硬编码凭据引起的问题的影响。 Here is an article with some examples。
如果凭据不是代码的一部分,这就提出了如何向应用程序提供凭据的问题。这取决于您的应用程序运行的平台。例如,如果您在某个云服务上托管您的应用程序,该服务将有一种机制以保存的方式存储凭据并将它们注入到应用程序的操作系统环境中。为了提供一个具体的例子,这里是文档 how to provide credentials for an app hosted on Heroku。 在您的应用程序代码中,您可以从环境中访问它们。例如。对于 Java,您可以使用 getenv
String apiPassword = getenv("API_PASSWORD");
此处 API_PASSWORD
需要由您的应用的托管机制在环境中提供。
我写了一篇关于该主题的博客文章,更详细地介绍了该主题:Keep passwords out of source code - why and how。
答案 6 :(得分:-1)
如果您无法信任您的程序正在运行的环境,但需要通过普通密码或证书进行身份验证,则无法保护凭据。您可以做的最多就是使用其他答案中描述的方法对它们进行模糊处理。
作为一种解决方法,我会通过您可以信任的代理运行RESTful api的所有请求,并从那里进行明文密码验证。