基本認証/基本証明書を使用するRESTful APIからプルしようとすると、そのユーザー名とパスワードをプログラムに保存する最良の方法は何でしょうか?今はただ平文でそこに座っています。
UsernamePasswordCredentials creds = new UsernamePasswordCredentials("myName@myserver","myPassword1234");
これを行うには、よりセキュリティに配慮した方法がありますか?
ありがとう
内から外への考え方で、プロセスを保護するためのいくつかの手順を以下に示します。
最初のステップでは、パスワード処理をString
から_character array
_に変更する必要があります。
これは、String
がimmutable
オブジェクトであるため、オブジェクトがnull
に設定されていても、データはすぐには消去されないためです。データは代わりにガベージコレクション用に設定されます。これは、悪意のあるプログラムがそのString
(パスワード)データに駆除される前にアクセスする可能性があるため、セキュリティ上の問題が生じます。
これが、 SwingのJPasswordFieldのgetText()
メソッドが非推奨であり、 getPassword()
が文字配列を使用する である主な理由です。
2番目の手順は、認証プロセス中に一時的にのみ資格情報を復号化して資格情報を暗号化することです。
これは、最初のステップと同様に、脆弱性時間を可能な限り短くすることを確実にします。
資格情報はハードコード化せず、代わりに、構成ファイルやプロパティファイルなど、一元化された構成可能な保守容易な方法で保存することをお勧めします。
ファイルを保存する前に資格情報を暗号化する必要があります。さらに、ファイル自体に2番目の暗号化を適用できます(資格情報に2層の暗号化、他のファイルコンテンツに1層の暗号化)。
上記の2つの暗号化プロセスはそれぞれ、それ自体を多層化できることに注意してください。各暗号化は、概念的な例として、 Triple Data Encryption Standard(別名TDESおよび3DES) の個別のアプリケーションにすることができます。
ローカル環境を適切に保護した後(ただし、決して「安全」ではないことを忘れないでください!)、3番目のステップは、 TLS(Transport Layer Security)またはSSL(Secure Sockets Layer)を使用して、送信プロセスに基本的な保護を適用することです) 。
4番目のステップは、他の保護方法を適用することです。
たとえば、プログラムを Ms。Eve、Mr。Mallory、または他の誰かが取得した場合に、セキュリティ対策が(短時間であっても)公開されるのを避けるために、難読化手法を "to-use"コンパイルに適用します。 (悪者) および逆コンパイル。
更新1:
@ Damien.Bellのリクエストにより、最初と2番目のステップをカバーする例を以下に示します。
_ //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.に関する他の質問がありますが、それは私の答えを大きすぎます(最後にサンプリング)これらの手順の "How to"で既に指示されており、はるかに適切であり、個々の手順の実装に関するより良い説明とサンプリングを提供しています。
基本認証を使用している場合は、SSLを使用してbase64でエンコードされたプレーンテキストで資格情報を渡さないようにする必要があります。パケットをスニッフィングする人が資格情報を簡単に取得できるようにしたくありません。また、ソースコードに資格情報をハードコーディングしないでください。それらを構成可能にします。設定ファイルからそれらを読んでください。資格情報を構成ファイルに保存する前に暗号化し、構成ファイルから資格情報を読み取った後、アプリで資格情報を復号化する必要があります。
加えて、おそらく私が忘れていた千のこと:)
一般に、資格情報を暗号化することは良いアドバイスではありません。暗号化されたものは解読できます。一般的なベストプラクティスは、パスワードを salted hash として保存することです。ハッシュは復号化できません。 Rainbow Tables でブルートフォース推測を無効にするために、塩が追加されます。すべてのuserIdが独自のランダムなソルトを持っている限り、攻撃者はソルトのすべての可能な値に対して一連のテーブルを生成する必要があり、宇宙の寿命内でこの攻撃をすばやく不可能にします。これは、パスワードを忘れた場合にWebサイトが一般にパスワードを送信できない理由ですが、パスワードを「リセット」することしかできません。パスワードは保存されず、ハッシュのみが保存されます。
パスワードハッシュを自分で実装することはそれほど難しくありませんが、解決するのは非常に一般的な問題であり、他の多くの人があなたのためにそれを行っています。 jBcrypt 使いやすいことがわかりました。
パスワードのブルートフォース推測に対する追加の保護として、間違ったパスワードでのログイン試行が一定回数行われた後、ユーザーIDまたはリモートIPが数秒間待機するのが一般的なベストプラクティスです。これがなければ、ブルートフォース攻撃者は、サーバーが処理できる数の1秒あたりのパスワードを推測できます。 10秒ごとに100個または100万個のパスワードを推測できるかどうかには大きな違いがあります。
ソースコードにユーザー名とパスワードの組み合わせが含まれているという印象を受けます。これは、パスワードを変更したい場合は、サービスを再コンパイル、停止、および再起動する必要があることを意味します。また、ソースコードを入手したユーザーもパスワードを持っていることを意味します。一般的なベストプラクティスは、決してこれを行うことではなく、資格情報(ユーザー名、パスワードハッシュ、パスワードソルト)をデータストアに保存することです