处理源代码中用于身份验证的密码


78

假设我试图从使用基本身份验证/基本证书的RESTful API中提取信息,那么在程序中存储该用户名和密码的最佳方法是什么?现在,它只是以纯文本格式坐在那里。

UsernamePasswordCredentials creds = new UsernamePasswordCredentials("myName@myserver","myPassword1234");

有某种更安全的方法吗?

谢谢


4
答案取决于以下几件事:是否要分发应用程序?用户/密码基于应用程序的用户,还是某种API密钥?您要保护本地用户的用户名/密码(某种DRM)吗?
parasietje 2012年

它实际上是一个在后端运行的程序,但实际上与样式有关。我不应该使用明文形式存储分类级别信息的帐户的用户名/密码。
A_Elric 2012年

2
看看这个线程stackoverflow.com/questions/12198228/…,您将获得基本的想法。
鲨鱼2012年

Answers:


112

重要的提示:

如果您总体上设计身份验证系统,则即使密码已加密,也不应存储密码。您存储一个哈希,并检查登录期间提供的密码是否匹配相同的哈希。这样,数据库上的安全漏洞可避免暴露用户密码。

如此说来,从内到外的心态,这是一些保护您的过程的步骤:


第一步,您应该将密码处理方式从更改Stringcharacter array

原因是aString是一个immutable对象,因此即使将对象设置为null,也不会立即清除其数据。而是将数据设置为垃圾收集,这会带来安全问题,因为恶意程序可能会String在清除之前(密码)数据访问该数据。

这是为什么不推荐使用Swing的JPasswordFieldgetText()方法以及为什么getPassword()使用字符数组的主要原因。


第二步是加密您的凭据,仅在身份验证过程中暂时将其解密。或者在服务器端对它们进行哈希处理,存储该哈希,然后“忘记”原始密码。

与第一步类似,这可以确保您的漏洞时间尽可能短。

建议不要对凭据进行硬编码,而应以集中,可配置且易于维护的方式(例如配置或属性文件或数据库)存储它们。

保存文件之前,应先对凭据进行加密,此外,还可以对文件本身进行第二次加密(对凭据进行2层加密,对其他文件内容进行1层加密)。

注意,上面提到的两个加密过程中的每个过程都可以是多层的。作为概念示例,每种加密都可以是三重数据加密标准(AKA TDES和3DES)的单独应用。


在对本地环境进行了适当的保护(但要记住,它永远不会“安全”!)之后,第三步是通过使用TLS(传输层安全性)或SSL(安全套接字层)对传输过程应用基本保护。


第四步是应用其他保护方法。

例如,将模糊处理技术应用于您的“使用”编译器,以防(即使是很快)暴露安全措施,以防万一您的程序是夏娃女士,马洛里先生或其他人(恶意软件,家伙)并反编译。


更新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上的其他问题已经针对这些步骤的“如何做”,更加合适,并且为实施这些问题提供了更好的解释和抽样。每个步骤。


3
[*]-@ Damien.Bell为了不让您的请求无人看管,我提供了一个包含第一步(〜)和第二步的示例。---至于为什么不是所有步骤都很好,正如您所看到的那样,您不能仅凭一小段代码来进行采样;而且即使是部分伪编码,网络保护的示例所需要的甚至比本地范围的示例还要多。模糊处理也有很多种实现方法,尽管它在概念上很简单,但实际上它被应用于源代码本身意味着很难在示例中进行解释。
XenoRo

2
最后,在您的源代码上运行混淆工具(如ProGuard)。众所周知,Java字节码易于拆解和分析。混淆是安全性蛋糕上的锦上添花,它使某人进行反向工程代码和潜在地破解您的安全性措施变得更加困难。请参阅:proguard.sourceforge.net/index.html#manual/introduction.html
Jarrod Smith,

1
@ Woot4Moo-据我了解,由于他正试图非本地实体(服务器)中提取信息,因此从认证者(服务器)的角度来看,认证的范围不是认证过程的范围,而是从客户端的角度来看。---这样,客户端必须在存储和传输中保护凭据,但应按原样发送凭据。传输安全性由第三步处理,该步骤固有具有加密,散列和消息摘要功能。___服务器是可进行这种哈希比较的位置,出于安全原因,客户端不应执行服务器职责过程。
XenoRo 2012年

1
@Roland过去一年左右,我没有用Java编写任何程序,所以我不记得要CharBuffer特别费力,但是基本上,如果它不是一成不变的(可以将它的内部数据重写null为零或不加零,等待GC),那么只要您不忘记清理它就可以使用它。
XenoRo

1
我的源代码中包含所有解密和加密信息(包括盐),这样安全吗?
德尔伯先生(Herr Derb)'18

7

如果您使用的是基本身份验证,则应将其与SSL结合使用,以避免以base64编码的纯文本形式传递您的凭据。您不想让有人嗅探您的数据包以获取您的凭据变得容易。另外,请勿在您的源代码中对您的凭据进行硬编码。使它们可配置。从配置文件中读取它们。您应先将凭据加密,然后再将其存储在配置文件中,并且一旦从配置文件中读取凭据,您的应用程序应解密凭据。


1
您能否提供一些有关如何以编程方式执行此操作的示例?
A_Elric 2012年


2
  1. 初始化请求的安全计算机(您的计算机)。如果那台机器不安全,没有任何东西可以保护您。这是完全独立的主题(最新软件,正确配置,强密码,加密交换,硬件嗅探器,物理安全性等)
  2. 保护您的存储用于存储凭据的介质应被加密。解密的凭据应仅存储在受保护机器的内存中
  3. 维护硬件的人们必须受到信任(可能是最薄弱的环节)
  4. 他们也应该知道的越少越好。这是对橡胶软管密码分析的保护
  5. 您的凭据应符合所有安全建议(适当的长度,随机性,单一目的等)
  6. 您与远程服务的连接必须得到保护(SSL等)
  7. 您的远程服务必须是受信任的(请参见第1-4点)。加上它应该容易被黑客入侵(如果您的数据/服务不安全,那么保护您的凭据就毫无意义)。而且它不应该存储您的凭据

再加上大概一千件事我忘记了:)


您的答案以一种概括但非常清晰且“可遵循的”方式涵盖了客户端和服务器端的保护步骤。我很喜欢它![+1] ---我认为有些事情应该做一点解释,并且由于目前还存在一些拼写和格式问题,因此我可以自由地进行编辑。---总体结构以及大部分文本均未更改。我只是添加了我认为缺少的内容,并重新组织了现有的文本以使其适合。我希望你不要介意。
XenoRo 2012年

我不介意拼写,链接,语法等。谢谢。但是,如果您想添加一些东西,请不要更改我的答案。如果您觉得缺少某些东西,请添加评论或创建自己的答案。我更喜欢只用我自己的话签名
piotrek

我明白。---好吧,我的编辑并没有真正改变您答案的含义。大多数情况是修复拼写和格式,而需要修复的格式无论如何都需要对文本进行少量更改。其他一些解释只是对已经说过的内容的扩展。---在任何情况下,请修正拼写(主要的问题是词组开头的大写)和格式(正确地将“主题”与“内容”分开),并对文本进行必要的调整。另外,请参阅#7的“倾向”。---而且,当然,这样做时要考虑其他因素。
XenoRo

1

加密凭据通常不是一个好建议。加密的东西可以解密。常见的最佳做法是将密码存储为加盐的散列。散列无法解密。添加盐是为了克服用Rainbow Tables进行的蛮力猜测。只要每个userId都有自己的随机盐,攻击者就必须为盐的每个可能值生成一组表,从而迅速使这种攻击在宇宙的生命周期内变得不可能。这就是为什么如果忘记了密码,网站通常无法将密码发送给您的原因,而它们只能“重置”密码。他们没有存储您的密码,只有一个哈希值。

密码哈希并不是很难实现的自己,但是解决许多人为您完成密码哈希是一个普遍的问题。我发现jBcrypt易于使用。

作为防止暴力破解密码的一种额外保护措施,通常的最佳做法是强制用户ID或远程IP在使用密码错误进行一定次数的登录尝试后等待几秒钟。否则,暴力攻击者每秒可以猜测出服务器可以处理的密码数量。每10秒可以猜测100个密码或100万个密码之间有很大的区别。

我觉得您在源代码中包含了用户名/密码组合。这意味着,如果您想更改密码,则必须重新编译,停止并重新启动服务,这也意味着,拥有源代码的任何人也都将拥有您的密码。常见的最佳做法是从不执行此操作,而是将凭据(用户名,密码哈希,密码盐)存储在数据存储区中


1
我仍然不确定,但我认为“ ...我正试图RESTful api中提取...”表明OP并未在谈论服务器端环境。我相信他是在谈论通过服务器进行身份验证的客户端应用程序。__因此,客户端应仅保护凭据的存储(加密)并将其安全地发送到服务器(TSL / SSL-本质上应用加密和消息摘要)___消息摘要(用于注册或比较) )应仅在服务器端执行,否则将是不安全的。___全部都在我的回答中。
XenoRo

另外,您的答案还表明使用了可能已经过时的API(jBcrypt-它在beta v0.3上,最后一次于2010年1月更新,这可能表明该项目已经失效)。Java已经拥有自己的标准消息摘要类,并且我认为对第三方API并没有必要。
XenoRo 2012年

看来您对客户端与服务器端的混淆是正确的。我仍然建议将凭据放在数据存储中,而不是在源代码中,但是在这种情况下,您需要加密而不是哈希是正确的。
Mzzl 2012年

Bcrypt不是消息摘要,而是基于河豚的密钥生成方案。我使用它作为SpringSecurity的一部分,它非常活跃。普通的消息摘要算法(例如SHA-1或MD5)不用于密码哈希,而是用于快速哈希。如果您需要尽可能快地哈希视频或文本块,则可以使用这些或更现代的替代方法。如果您对哈希密码感兴趣,那么速度就是您的敌人。使用的哈希算法越快,暴力攻击就可以越快成功。
Mzzl 2012年

嗯 一些Google搜索表明我Blowfish是一种加密(可解密),而jBcrypt的页面指示它使用了基于Blowfish的消息摘要(加密哈希函数)……我很困惑。___ SpringSecurity还活着,但Bcrypt可能还没有;他们是独立的项目。___不管怎么说,Java 1.7已经包含了河豚密码,并且Security类的模块化结构使其可以轻松实现,security.Provider甚至在较旧的版本中也是如此,因此我仍然看不到需要第三方API。
XenoRo

-1

如果您不能信任程序所运行的环境,但是需要通过普通密码或证书进行身份验证,则无法采取任何措施来保护凭据。您所能做的就是用其他答案中描述的方法来混淆它们。

作为一种解决方法,我将通过您可以信任的代理运行对RESTful api的所有请求,并从那里进行明文密码验证。


“如果您不能信任程序在其中运行的环境,...,您将无法采取任何措施来保护您的凭据。” -如果是这样,那么几乎每个具有凭据“自动填充”选项的应用程序都将面临很大的麻烦。___许多诸如多人游戏和基于Web的应用程序之类的两端应用程序(此问题是?)在本地存储帐户凭据,并且它们几乎没有任何严重的安全问题。___无论环境如何,数据永远不会100%安全。受信任的环境只是另一个安全性(“更安全”)步骤。
XenoRo

好了,在给定的情况下,您可以混淆凭据,但不能达到100%(加密)安全性。您最希望得到的是使攻击者获得明文密码的过程变得如此复杂,以至于他们不值得这样做。要获取典型的基于Web的应用程序的存储密码,只需进入浏览器的选项菜单,然后选择“显示密码”。
Twilite,2012年

无论在这种情况下还是在任何其他情况下,您都无法达到100%的安全性。这是因为最后,所有这些都归结为的顺序01在内存中的顺序,这是根据一组特定的逻辑规则实现的,而这些逻辑规则固有地总是可以逆转的。___密码安全性的基础始终是,而且将来也始终是,“使其变得如此困难以至于不值得付出努力。” ___最后,您误以为浏览器的自动填充/登录(适用于网站)和应用程序的自动身份验证/登录(仅保存到加密文件中)。
XenoRo 2012年

您应该阅读密码学。不可逆地加密数据的方法有很多种,只需看看“单向哈希”(md5)或公共密钥加密,就无法解密加密的数据,即使您同时拥有加密的数据和加密密钥。使用这些方法,您可以获得100%的实际安全性。
Twilite

其实没有 就像我说的那样,这些方法遵循一组特定的逻辑规则,并且这些逻辑规则可以逆转。___就密码哈希函数而言,如果黑客知道生成哈希的规则集,则他可以重新获得原始数据的几位,并大致了解原始数据的长度。---蛮力和猜测很多,但并不是100%坚不可摧。而且距离100%安全性的无限尝试还很遥远___我认为任何黑客都不会费力地尝试艰苦的工作。无论付出多少回报,付出的努力都远远不够。
XenoRo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.