Обработка паролей, используемых для аутентификации в исходном коде


предполагая, что я пытаюсь извлечь из RESTful api, который использует обычную аутентификацию / базовые сертификаты, каков был бы лучший способ сохранить это имя пользователя и пароль в моей программе? Сейчас он просто сидит там в открытом виде.

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

есть ли какой-то способ сделать это, который больше ориентирован на безопасность?

спасибо

5 55

5 ответов:

С внутренним к внешнему мышлению, вот несколько шагов, чтобы защитить ваш процесс:


первый шаг, вы должны изменить свой пароль-обращение от String to character array.

причина этого в том, что a String данные объекта не будут очищены немедленно, даже если объект установлен в null; данные, установленные для сбора мусора, и это создает проблемы безопасности, так как вредоносные программы могут получить доступ к этому String (пароль) данных прежде чем он будет очищен.

это главная причина, почему качели JPasswordField в getText() метод устарел, и почему getPassword() использует массивы символов.


второй шаг заключается в шифровании учетных данных, только расшифровывая их временно во время процесса аутентификации.

это, как и первый шаг, гарантирует, что ваша уязвимость-время как можно меньше.

рекомендуется учетные данные не являются жестко закодированными, и вместо этого вы храните их централизованным, настраиваемым и легко поддерживаемым способом, таким как файл конфигурации или свойств.

вы должны зашифровать свои учетные данные перед сохранением файла, и кроме того, вы можете применить второе шифрование к самому файлу (2-слойное шифрование к учетным данным и 1-слойное к другому содержимому файла).

обратите внимание, что каждый из двух упомянутых выше процессов шифрования может быть многослойные сами по себе. Каждое шифрование может быть индивидуальным приложением тройной стандарт шифрования данных (он же TDES и 3DES), как концептуальный пример.


после того, как ваша локальная среда будет должным образом защищена (но помните, что это никогда не будет "безопасно"!), третий шаг-применить базовую защиту к вашему процессу передачи, используя TLS (Transport Layer Security) или SSL (Secure Sockets Layer).


четвертый шаг применять другие методы защиты.

например, применяя методы обфускации к вашей" используемой " компиляции, чтобы избежать (даже если в ближайшее время) воздействия ваших мер безопасности в случае, если ваша программа получена Мисс Ева, Мистер Мэллори, или кто-то еще (плохие парни) и декомпилированный.


обновление 1:

By @Damien.Запрос Белла, вот пример, который охватывает первый и второй шаги:

    //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. уже направлены на "как" из этих шагов, будучи гораздо более подходящим, и предлагая гораздо лучшее объяснение и выборка по реализации каждого отдельного шага.

Если вы используете basic auth, вы должны связать это с SSL, чтобы избежать передачи учетных данных в кодированном простом тексте base64. Вы не хотите, чтобы кто-то обнюхивал ваши пакеты, чтобы получить ваши учетные данные. Кроме того, не жестко кодируйте свои учетные данные в исходном коде. Сделать их настраиваемыми. читать их из файла config. Вы должны зашифровать учетные данные перед их сохранением в файле конфигурации, и ваше приложение должно расшифровать учетные данные после их чтения из конфигурации файл.

  1. безопасный компьютер, который инициализирует запрос (ваш компьютер). если эта машина небезопасна, ничто не защитит вас. это совершенно отдельная тема (современное программное обеспечение, правильно настроенное, надежные пароли, зашифрованный своп, аппаратные снифферы, физическая безопасность и т. д.)
  2. безопасное хранение носитель, используемый для хранения учетных данных, должен быть зашифрован. расшифрованные учетные данные должны храниться только в ОЗУ вашего защищенного компьютера
  3. люди, которые поддерживайте, что аппаратное обеспечение должно быть надежным (вероятно, самое слабое звено)
  4. они также должны знать как можно меньше. это защита от криптоанализа с резиновым шлангом
  5. ваши учетные данные должны соответствовать всем рекомендациям по безопасности (правильная длина, случайность, одна цель и т. д.)
  6. ваше соединение с удаленной службой должно быть защищено (SSL и т. д.)
  7. ваша удаленная служба должна быть надежной (см. пункты 1-4). плюс он должен быть склонен к взлому (если ваш данные / служба небезопасны, тогда защита ваших учетных данных бессмысленна). кроме того, он не должен хранить ваши учетные данные

плюс, наверное, тысячи вещей, о которых я забыл:)

обычно не рекомендуется шифровать учетные данные. То, что зашифровано, может быть расшифровано. Обычно рекомендуется хранить пароли в виде соленые хэш.Хэш не может быть расшифрован. Соль добавляется, чтобы победить грубую силу угадывания с Радужные Таблицы. Пока каждый идентификатор пользователя имеет свою собственную случайную соль, злоумышленник должен будет генерировать набор таблиц для каждого возможного значения соли, быстро делая эту атаку невозможной в течение срока службы Вселенной. Именно по этой причине веб-сайты обычно не могут отправить вам свой пароль, если вы его забыли, но они могут только "сбросить" его. У них нет вашего пароля, только его хэш.

хэширование паролей-это не очень сложно реализовать себя, но это такая распространенная проблема, что многие другие сделали это за вас. Я нашел jBcrypt прост в использовании.

как дополнительная защита от грубой силы угадывания паролей, обычно рекомендуется заставить идентификатор пользователя или удаленный IP-адрес подождать несколько секунд после определенного количества попыток входа с неправильным паролем. Без этого злоумышленник грубой силы может угадать столько паролей в секунду, сколько может обработать ваш сервер. Существует огромная разница между возможностью угадать 100 паролей за 10-секундный период или миллион.

У меня сложилось впечатление, что вы включили комбинацию имени пользователя/пароля в свой исходный код. Это означает, что если вы когда-либо если вы хотите изменить пароль, вам придется перекомпилировать, остановить и перезапустить свой сервис, а это также означает, что любой, кто получает доступ к вашему исходному коду, также имеет ваши пароли. Обычно рекомендуется никогда не делать этого, но хранить учетные данные (имя пользователя, хэш пароля, соль пароля) в вашем хранилище данных

Если вы не можете доверять среде, в которой работает ваша программа, но вам нужно пройти аутентификацию с помощью простых паролей или сертификатов, вы ничего не можете сделать для защиты своих учетных данных. Самое большее, что вы можете сделать, это запутать их с помощью методов, описанных в других ответах.

в качестве обходного пути я бы запускал все запросы к RESTful api через прокси-сервер, которому вы можете доверять, и оттуда выполнял аутентификацию с открытым текстом пароля.