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

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

UsernamePasswordCredentials creds = new UsernamePasswordCredentials("[email protected]","myPassword1234");

Есть ли способ сделать это с большей осторожностью?

Спасибо

Ответ 1

С внутренним мышлением вот несколько шагов для защиты вашего процесса:


Первый шаг, вы должны изменить вашу обработку пароля с String на character array.

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

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


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

Это, подобно первому шагу, гарантирует, что время вашей уязвимости будет как можно меньше.

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

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

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


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


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

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


ОБНОВЛЕНИЕ 1:

По запросу @Damien.Bell, вот пример, который охватывает первый и второй этапы:

    //These will be used as the source of the configuration file 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 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 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 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 уже направлены на "Как" на этих шагах, будучи гораздо более подходящими и предлагая намного лучшее объяснение и выборку по реализации каждый отдельный шаг.

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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