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

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

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

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

Спасибо

7 ответов

Решение

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


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

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

Это основная причина, по которой Swass's JPasswordField'sgetText() метод устарел, и почему 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'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 уже направлены на "Как" на этих шагах, будучи гораздо более подходящими и предлагая намного лучшее объяснение и выборку по реализации каждый отдельный шаг.

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

Почему бы не хранить учетные данные в исходном коде

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

Можно с уверенностью сказать, что сотни миллионов пользователей уже пострадали от проблем, вызванных жестко заданными учетными данными . Вот статья с некоторыми примерами .

Как предоставить учетные данные для вашего приложения

Если учетные данные не являются частью кода, возникает вопрос, как вы можете предоставить учетные данные своему приложению. Это зависит от платформы, на которой работает ваше приложение. Например, если вы размещаете свое приложение в какой-либо облачной службе, эта служба будет иметь механизм для сохранения учетных данных и внедрения их в среду операционной системы вашего приложения. Чтобы привести конкретный пример, вот документация о том, как предоставить учетные данные для приложения, размещенного на Heroku. В вашем коде приложения вы можете получить к ним доступ из среды. Например, для Java вы можете использовать getenv

      String apiPassword = getenv("API_PASSWORD");

Здесь должен быть предоставлен в среде механизмом размещения вашего приложения.

Дальнейшее чтение

Я написал статью в блоге на эту тему, которая более подробно освещает эту тему: Хранить пароли вне исходного кода — почему и как .

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

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

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

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

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

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

Почему люди говорят о хешировании. OP хочет сохранить учетные данные своих пользователей для доступа к внешнему ресурсу. Хеширование его пароля не поможет.

Теперь это не мешает. Я бы просто простил передовой опыт для каждого слоя.

1. Сохранение вашего пароля в приложении Java.: Сохранить как массив Char. Создайте класс хранилища паролей и сохраните пароль в виде хэш-карты с ключом в качестве ресурса, к которому вы хотите получить доступ, и значением как некоторый объект, содержащий имя пользователя и пароль. Ограничьте точку входа для этого API с некоторой аутентификацией Пример: примите учетные данные вошедших в систему пользователей, чтобы проверить уровень доступа этого пользователя для этого ресурса (просто сопоставьте пользователя со списком паролей, к которым они могут получить доступ. Если у вас много паролей, создайте группу и сопоставьте ключ карты паролей с этой группой) Все, что выходит за рамки этого для хранения пароля, зависит от того, насколько вы параноидально относитесь к самой jvm, чтобы его утечь.

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

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

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

Другие вопросы по тегам