在源代码中处理用于身份validation的密码
假设我试图从使用基本authentication/基本证书的RESTful api中取出,那么在我的程序中存储该用户名和密码的最佳方法是什么? 现在它只是坐在那里明文。
UsernamePasswordCredentials creds = new UsernamePasswordCredentials("myName@myserver","myPassword1234");
有没有办法做到这一点更安全的思想?
谢谢
从内在到外在的心态,这里有一些步骤来保护你的过程:
第一步,你应该改变你的密码处理从String
到character array
。
这样做的原因是,即使对象设置为null
, String
对象的数据也不会被立即清理; 数据被设置为垃圾收集,而这会造成安全问题,因为恶意程序在清理之前可能会访问该String
(密码)数据。
这是Swing的JPasswordField的getText()
方法被弃用的主要原因,为什么getPassword()
使用字符数组 。
第二步是对您的凭证进行encryption,在validation过程中只对其进行临时解密。
这与第一步类似,可以确保您的漏洞时间尽可能小。
build议您的凭证不是硬编码的,而是以集中,可configuration和易于维护的方式(例如configuration或属性文件)存储它们。
您应该在保存文件之前encryption您的凭证,此外,您可以对文件本身应用第二次encryption(对凭证进行双层encryption,对其他文件内容进行1层encryption)。
请注意,上述两个encryption过程中的每一个都可以是多层次的。 作为概念性的例子,每个encryption可以是三重数据encryption标准(AKA TDES和3DES)的单独应用。
当你的本地环境得到适当的保护(但记住,它永远不会“安全”!),第三步是通过使用TLS(传输层安全)或SSL(安全套接字层)对传输过程应用基本保护。
第四步是应用其他保护方法。
例如,将混淆技术应用到“易用”编译中,以避免(即使很短时间)暴露您的安全措施,以防您的程序被Eve女士,Mallory先生或其他人(恶意程序员)家伙)和反编译。
更新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()); }
处理每一个保护步骤的完整例子将远远超过我认为对于这个问题是合理的,因为它是关于“什么是步骤” ,而不是“如何应用它们” 。
这将远远超出我的答案(最后是抽样),而关于SO的其他问题已经针对这些步骤的“如何” ,更加合适,并且对实施方面提供了更好的解释和抽样每个单独的步骤。
如果您使用的是基本身份validation,则应将其与SSL结合使用,以避免以base64编码的纯文本传递凭证。 你不想让别人嗅探你的数据包来获取你的证书。 另外,不要在您的源代码中硬编码您的凭据。 使他们可configuration。 从configuration文件中读取它们。 您应该在将证书存储在configuration文件中之前对其进行encryption,并且一旦从configuration文件读取证书,您的应用程序应该解密证书。
- 安全的计算机初始化请求(您的计算机)。 如果那台机器不安全,没有什么能保护你。 这是完全独立的主题(最新的软件,正确configuration,强密码,encryption交换,硬件嗅探器,物理安全等)
- 保护您的存储您用于存储您的凭据的媒体应encryption。 解密后的凭证只能存储在受保护机器的内存中
- 维护硬件的人必须是可信的(可能是最薄弱的环节)
- 他们也应该尽可能less知道。 这是橡胶密封分析的保护
- 你的证书应该满足所有的安全build议(适当的长度,随机性,单一目的等)
- 您的远程服务连接必须安全(SSL等)
- 您的远程服务必须是可信的(请参阅第1-4点)。 加上它应该是黑客容易(如果您的数据/服务是不安全的,那么保护您的凭证是毫无意义的)。 另外它不应该存储您的凭据
加上大概有一千个我忘了的东西:)
encryption凭据通常不是很好的build议。 某些被encryption的东西可以被解密。 常见的最佳做法是将密码存储为盐味散列 。散列不能被解密。 添加盐是为了用彩虹表击败蛮力猜测。 只要每个用户ID都有自己的随机盐,攻击者就不得不为盐的每一个可能的值生成一组表,这种攻击在宇宙的生命周期内很快就不可能实现。 这就是为什么网站通常不能发送给你密码,如果你忘记了,但他们只能“重置”它。 他们没有存储你的密码,只有它的散列。
密码哈希法不是很难实现,但是解决这个问题是一个很常见的问题,无数其他人为你做了。 我发现jBcrypt易于使用。
作为防止暴力猜测密码的额外保护措施,通常的最佳做法是强制用户ID或远程IP在使用错误密码进行一定次数的login尝试后等待几秒钟。 如果没有这个,暴力攻击者可以猜测你的服务器每秒可以处理的密码数量。 能够猜测每10秒或100万次的100个密码之间有巨大的差异。
我觉得你已经在你的源代码中包含了用户名/密码组合。 这意味着如果您想更改密码,则必须重新编译,停止并重新启动服务,这也意味着任何持有源代码的人也都有密码。 常见的最佳做法是不要这样做,而是将凭据(用户名,密码哈希,密码salt)存储在数据存储中
如果您不能相信您的程序正在运行的环境,但需要通过简单密码或证书进行身份validation,则无法保护您的凭据。 你可以做的最多的是用其他答案中描述的方法对它们进行混淆。
作为一个解决方法,我会运行所有的请求到RESTful api通过一个你可以信任的代理,并从那里进行明文密码authentication。