基本認証/基本証明書を使用するRESTfulAPIからプルしようとしていると仮定すると、そのユーザー名とパスワードをプログラムに保存するための最良の方法は何でしょうか?今は平文で座っているだけです。
UsernamePasswordCredentials creds = new UsernamePasswordCredentials("myName@myserver","myPassword1234");
よりセキュリティを重視するこれを行う方法はありますか?
ありがとう
基本認証/基本証明書を使用するRESTfulAPIからプルしようとしていると仮定すると、そのユーザー名とパスワードをプログラムに保存するための最良の方法は何でしょうか?今は平文で座っているだけです。
UsernamePasswordCredentials creds = new UsernamePasswordCredentials("myName@myserver","myPassword1234");
よりセキュリティを重視するこれを行う方法はありますか?
ありがとう
認証システム全体を設計している場合は、たとえ暗号化されていても、パスワードを保存しないでください。ハッシュを保存し、ログイン時に提供されたパスワードが同じハッシュと一致するかどうかを確認します。そうすることで、データベースのセキュリティ違反により、ユーザーのパスワードが公開されるのを防ぐことができます。
そうは言っても、データをそのまま(この場合はパスワード)保存し、次に内部から外部への考え方で保存する状況では、プロセスを保護するためのいくつかの手順を次に示します。
最初のステップでは、パスワード処理をからString
に変更する必要がありますcharacter array
。
これは、aString
がimmutable
オブジェクトであるため、オブジェクトがnull
;に設定されていても、そのデータはすぐにはクレンジングされないためです。String
データは代わりにガベージコレクション用に設定されており、悪意のあるプログラムがクリーンアップされる前にその(パスワード)データにアクセスする可能性があるため、セキュリティ上の問題が発生します。
これが、 SwingのJPasswordFieldのgetText()
メソッドが非推奨になり、getPassword()
文字配列を使用する主な理由です。
2番目のステップは、認証プロセス中に一時的にクレデンシャルを復号化するだけで、クレデンシャルを暗号化することです。または、サーバー側でハッシュするには、そのハッシュを保存し、元のパスワードを「忘れる」。
これは、最初のステップと同様に、脆弱性時間を可能な限り短くすることを保証します。
クレデンシャルはハードコーディングせず、代わりに、構成ファイルやプロパティファイル、データベースなど、一元化された構成可能で保守が容易な方法で保存することをお勧めします。
ファイルを保存する前にクレデンシャルを暗号化する必要があります。さらに、ファイル自体に2番目の暗号化を適用できます(クレデンシャルには2層の暗号化、他のファイルの内容には1層の暗号化)。
上記の2つの暗号化プロセスは、それぞれ多層化できることに注意してください。各暗号化は、概念的な例として、トリプルデータ暗号化標準(別名TDESおよび3DES)の個別のアプリケーションにすることができます。
ローカル環境が適切に保護された後(ただし、「安全」になることは決してありません!)、3番目のステップは、TLS(トランスポート層セキュリティ)またはSSL(Secure Sockets Layer)を使用して、送信プロセスに基本的な保護を適用することです。
4番目のステップは、他の保護方法を適用することです。
たとえば、「使用する」コンパイルに難読化手法を適用して、プログラムがイブさん、マロリーさん、または他の誰か(悪い-みんな)そして逆コンパイルしました。
更新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());
}
すべての保護ステップに対処する完全な例は、 「ステップとは何か」に関するものであり、「それらを適用する方法」に関するものではないため、この質問に対して私が合理的だと思うものをはるかに超えます。
それは私の答えをはるかに大きくします(最後にサンプリング)が、SOに関する他の質問はすでにそれらのステップの「方法」に向けられており、はるかに適切であり、個々のステップ。
基本認証を使用している場合は、それをSSLと組み合わせて、base64でエンコードされたプレーンテキストで資格情報が渡されないようにする必要があります。誰かがあなたのパケットを盗聴してあなたの資格情報を簡単に取得できるようにしたくはありません。また、ソースコードにクレデンシャルをハードコーディングしないでください。それらを構成可能にします。設定ファイルからそれらを読み取ります。クレデンシャルを構成ファイルに保存する前に暗号化する必要があります。アプリは、クレデンシャルを構成ファイルから読み取った後、クレデンシャルを復号化する必要があります。
それに加えて、おそらく私が忘れていた何千ものこと:)
一般に、クレデンシャルを暗号化することはお勧めできません。暗号化されたものは復号化できます。一般的なベストプラクティスは、パスワードをソルトハッシュとして保存することです。ハッシュは復号化できません。レインボーテーブルでブルートフォース推測を打ち負かすために塩が追加されます。すべてのuserIdが独自のランダムなソルトを持っている限り、攻撃者はソルトのすべての可能な値に対して一連のテーブルを生成する必要があり、ユニバースの存続期間内にこの攻撃を迅速に不可能にします。これが、パスワードを忘れた場合にWebサイトが通常パスワードを送信できない理由ですが、パスワードを「リセット」することしかできません。彼らはあなたのパスワードを保存しておらず、そのハッシュだけを保存しています。
パスワードハッシュを自分で実装することはそれほど難しいことではありませんが、解決するのは非常に一般的な問題であり、無数の他の人があなたのためにそれを行っています。jBcryptは使いやすいと思いました。
パスワードのブルートフォース推測に対する追加の保護として、間違ったパスワードで特定の回数のログイン試行が行われた後、userIdまたはリモートIPを数秒間待機させるのが一般的なベストプラクティスです。これがないと、ブルートフォース攻撃者はサーバーが処理できる数のパスワードを1秒あたりに推測できます。10秒間に100個のパスワードを推測できるか、100万個を推測できるかには大きな違いがあります。
ソースコードにユーザー名とパスワードの組み合わせが含まれているようです。これは、パスワードを変更したい場合は、サービスを再コンパイル、停止、再起動する必要があることを意味します。また、ソースコードを入手した人は誰でも、パスワードを持っていることを意味します。一般的なベストプラクティスは、これを行うことは決してありませんが、クレデンシャル(ユーザー名、パスワードハッシュ、パスワードソルト)をデータストアに保存することです。
一般に、クレデンシャルをソースコードに保存しないことをお勧めします。問題は、コードへのアクセスと、資格情報へのアクセス権を持つべき人が時間の経過とともに変化することが多いということです。プロジェクトがより成熟すると、通常、知る必要がないため、特定の資格情報を知る必要のない開発者がいます。さらに、コードはわずかに異なる目的で再利用されたり、オープンソースになることさえあります。また、コードベースがより複雑になるにつれて、コードの途中に埋め込まれているクレデンシャルを特定するのは非常に面倒になります。
何億人ものユーザーが、ハードコードされたクレデンシャルによって引き起こされる問題の影響をすでに受けていると言っても過言ではありません。ここにいくつかの例を含む記事があります。
クレデンシャルがコードの一部ではない場合、これにより、アプリケーションにクレデンシャルを提供する方法が疑問視されます。これは、アプリケーションが実行されているプラットフォームによって異なります。たとえば、アプリケーションをクラウドサービスでホストする場合、このサービスには、保存された方法でクレデンシャルを保存し、それらをアプリケーションのオペレーティングシステム環境に挿入するメカニズムがあります。具体的な例を示すために、Herokuでホストされているアプリのクレデンシャルを提供する方法のドキュメントを次に示します。アプリケーションコードでは、環境からそれらにアクセスできます。たとえば、Javaの場合はgetenvを使用できます
String apiPassword = getenv("API_PASSWORD");
ここでAPI_PASSWORD
は、アプリのホスティングメカニズムによって環境に提供する必要があります。
私はこのトピックをより詳細にカバーするトピックについてのブログ記事を書きました:パスワードをソースコードから遠ざけてください-理由と方法。
なぜ人々はハッシュについて話しているのですか。OPは、外部リソースにアクセスするためにユーザーの資格情報を保存したいと考えています。彼のパスワードをハッシュしても役に立ちません。
今では邪魔になりません。すべてのレイヤーの簡単なベストプラクティスを紹介します。
1。Javaアプリにパスワードを保存します。:Char配列として保存します。パスワードストアクラスを作成し、アクセスするリソースとしてキーを使用してハッシュマップとしてパスワードを保存し、ユーザー名とパスワードを含むオブジェクトとして評価します。いくつかの認証を使用して、エントリポイントをこのAPIに制限します。例:ログインしたユーザーのクレデンシャルを受け入れて、そのリソースに対するそのユーザーのアクセスレベルを検証します(ユーザーをアクセス可能なパスワードのリストにマップするだけです。多くの場合、グループを作成しますそして、passwordmapキーをそのグループにマップします)これを超えてパスワードを保存するものは、jvm自体がそれをリークすることについてどれほどパラノイアであるかによって異なります。
プログラムが実行されている環境を信頼できないが、プレーンなパスワードまたは証明書を使用して認証する必要がある場合、資格情報を保護するためにできることは何もありません。あなたができることのほとんどは、他の回答で説明されている方法でそれらを難読化することです。
回避策として、RESTful APIへのすべてのリクエストを、信頼できるプロキシを介して実行し、そこからクリアテキストのパスワード認証を実行します。