关于Java:在源代码中处理用于AUTH的密码

Handling passwords used for auth in source code

假设我试图从一个使用基本身份验证/基本证书的RESTfulAPI中提取,那么在我的程序中存储用户名和密码的最佳方法是什么?现在它只是纯文本的。

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

有没有比这更安全的方法?

谢谢


有了从内到外的心态,以下是一些保护你的过程的步骤:

第一步,您应该将密码处理从String更改为character array

这是因为Stringimmutable对象,因此即使对象设置为null对象,也不会立即清除其数据;而是将数据设置为垃圾收集,这会造成安全问题,因为恶意程序可能会在清除之前访问该String数据。d.

这就是Swing的jpasswordField的getText()方法被弃用的主要原因,也是getPassword()使用字符数组的原因。

第二步是加密您的凭证,只在身份验证过程中临时解密它们。

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

建议您不要对凭证进行硬编码,而是以集中、可配置和易于维护的方式存储凭证,例如配置或属性文件。

在保存文件之前,您应该加密您的凭据,另外,您可以对文件本身进行第二次加密(对凭据进行两层加密,对其他文件内容进行一层加密)。

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

在你的当地环境得到适当保护之后(但是记住,它从来都不是"安全的"!)第三步是通过使用tls(传输层安全性)或ssl(安全套接字层)对传输过程应用基本保护。

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

例如,在"要使用"编译中应用模糊技术,以避免(即使很快)暴露您的安全措施,以防您的程序被Eve女士、Mallory先生或其他人(坏人)获取并反编译。

更新1:

通过@damien.bell的请求,下面是一个示例,涵盖了第一步和第二步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
    //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:
"
+ readFile_NoDecryption(NO_ENCRYPTION) +"


"
);
        System.out.println("SINGLE LAYER ENCRYPTION:
"
+ readFile_NoDecryption(SINGLE_LAYER) +"


"
);
        System.out.println("DOUBLE LAYER ENCRYPTION:
"
+ readFile_NoDecryption(DOUBLE_LAYER) +"


"
);

        /*
         * 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]
"
+ decryptedContent +"


"
);
        //Decrypt second layer (secure data).
        for (String line : decryptedContent.split("
"
)) {
            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.上的其他问题已经针对这些步骤的"如何做",更为恰当,并对每个步骤的实施提供了更好的解释和抽样。


如果您使用的是basic auth,那么应该将其与ssl结合起来,以避免以base64编码的纯文本形式传递凭证。你不想让别人嗅探你的包以获取你的证书。另外,不要在源代码中硬编码您的凭证。使它们可配置。从配置文件中读取它们。您应该在将凭据存储在配置文件中之前对其进行加密,并且应用程序从配置文件中读取凭据后,应该对其进行解密。


  • 初始化请求的安全计算机(您的计算机)。如果那台机器不安全,什么也保护不了你。这是完全独立的主题(最新软件、正确配置、强密码、加密交换、硬件嗅探器、物理安全等)
  • 保护您的存储空间用于存储凭据的媒体应加密。解密的凭据应仅存储在安全计算机的RAM中。
  • 维护硬件的人必须得到信任(可能是最薄弱的环节)
  • 他们也应该知道的尽可能少。这是对橡胶软管密码分析的保护
  • 您的证书应满足所有安全建议(适当的长度、随机性、单一目的等)
  • 您与远程服务的连接必须是安全的(SSL等)
  • 您的远程服务必须是可信的(见第1-4点)。另外,它应该容易被黑客攻击(如果您的数据/服务不安全,那么保护您的凭证是毫无意义的)。另外,它不应该存储您的凭证
  • 加上我可能忘记了一千件事:)


    加密凭证通常不是一个好建议。加密的东西可以解密。常见的最佳实践是将密码存储为盐散列。无法解密散列。加盐是为了击败用彩虹表进行的野蛮猜测。只要每个用户ID都有自己的随机salt,攻击者就必须为salt的每一个可能值生成一组表,从而在宇宙的生命周期内迅速使这种攻击成为不可能。这就是为什么如果你忘记了密码,网站一般不能发送你的密码,但他们只能"重置"它的原因。他们没有存储你的密码,只有一个散列密码。

    密码散列不是很难实现您自己,但它是一个常见的问题来解决,无数其他人为您做了这件事。我发现jcrypt很容易使用。

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

    我觉得您在源代码中包含了用户名/密码组合。这意味着,如果您想更改密码,就必须重新编译、停止和重新启动您的服务,这也意味着任何拥有源代码的人都拥有您的密码。通常的最佳实践是永远不要这样做,而是将凭据(用户名、密码哈希、密码盐)存储在数据存储中。


    如果您不能信任您的程序正在运行的环境,但需要通过普通密码或证书进行身份验证,那么您就无法保护您的凭据。你能做的最多的就是用其他答案中描述的方法混淆它们。

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