パソコンで管理しているパスワードをスマホでもバックアップしたい。ただパスワードをテキストのまま保存するのはセキュリティー的にどうかと思うので暗号化して保存したい。
パソコンでのパスワードの保存はchromeにまかせていたり、バックアップにIDmanagerを使っている。ただandroid版がないのでスマホで管理ができない。アンドロイドアプリのパスワードマネージャーはいくらでもあるが、google以外だとどうも信用できないのと。Googleアカウント紛失時のバックアップとして使いたい。自分で作るなら、IDmanagerのXMLを読み込んでアプリ内部に暗号化して保持したい。
あなた:
android javaでデータを暗号化して保存するには
ChatGPT:
Android Java でデータを暗号化して保存する方法はいくつかありますが、代表的なのは以下のアプローチです。
ファイルや文字列を暗号化する場合に便利です。
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class CryptoUtil {
// パスワードとソルトからAESキーを生成
private static SecretKey generateKey(String password, String salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), "AES");
}
// 暗号化
public static String encrypt(String plainText, String password, String salt) throws Exception {
SecretKey secretKey = generateKey(password, salt);
// IV(初期化ベクトル)
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
// IVも一緒に保存する(先頭に付加)
byte[] encryptedIvAndText = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, encryptedIvAndText, 0, iv.length);
System.arraycopy(encrypted, 0, encryptedIvAndText, iv.length, encrypted.length);
return Base64.encodeToString(encryptedIvAndText, Base64.DEFAULT);
}
// 復号化
public static String decrypt(String cipherText, String password, String salt) throws Exception {
SecretKey secretKey = generateKey(password, salt);
byte[] decoded = Base64.decode(cipherText, Base64.DEFAULT);
// IVを取り出す
byte[] iv = new byte[16];
System.arraycopy(decoded, 0, iv, 0, iv.length);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
// 実際の暗号文を取り出す
byte[] encryptedBytes = new byte[decoded.length - iv.length];
System.arraycopy(decoded, iv.length, encryptedBytes, 0, encryptedBytes.length);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] original = cipher.doFinal(encryptedBytes);
return new String(original, "UTF-8");
}
}
import android.util.Base64;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class CryptoUtil {
// パスワードとソルトからAESキーを生成
private static SecretKey generateKey(String password, String salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), "AES");
}
// 暗号化
public static String encrypt(String plainText, String password, String salt) throws Exception {
SecretKey secretKey = generateKey(password, salt);
// IV(初期化ベクトル)
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
// IVも一緒に保存する(先頭に付加)
byte[] encryptedIvAndText = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, encryptedIvAndText, 0, iv.length);
System.arraycopy(encrypted, 0, encryptedIvAndText, iv.length, encrypted.length);
return Base64.encodeToString(encryptedIvAndText, Base64.DEFAULT);
}
// 復号化
public static String decrypt(String cipherText, String password, String salt) throws Exception {
SecretKey secretKey = generateKey(password, salt);
byte[] decoded = Base64.decode(cipherText, Base64.DEFAULT);
// IVを取り出す
byte[] iv = new byte[16];
System.arraycopy(decoded, 0, iv, 0, iv.length);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
// 実際の暗号文を取り出す
byte[] encryptedBytes = new byte[decoded.length - iv.length];
System.arraycopy(decoded, iv.length, encryptedBytes, 0, encryptedBytes.length);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] original = cipher.doFinal(encryptedBytes);
return new String(original, "UTF-8");
}
}
// 暗号化して保存
String encrypted = CryptoUtil.encrypt("保存したいデータ", "password123", "somesalt");
prefs.edit().putString("data", encrypted).apply();
// 復号化して読み込み
String decrypted = CryptoUtil.decrypt(prefs.getString("data", ""), "password123", "somesalt");
SharedPreferences prefs = getSharedPreferences("secure_prefs", MODE_PRIVATE);
// 暗号化して保存
String encrypted = CryptoUtil.encrypt("保存したいデータ", "password123", "somesalt");
prefs.edit().putString("data", encrypted).apply();
// 復号化して読み込み
String decrypted = CryptoUtil.decrypt(prefs.getString("data", ""), "password123", "somesalt");
パスワードをアプリ内に直書きするのは危険なので、より安全にするなら Android Keystore を使います。 Keystoreはデバイス内に秘密鍵を安全に保存し、鍵をアプリから直接取り出せません。
keyStore.load(null);
// AES鍵を生成
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(
new KeyGenParameterSpec.Builder("MyKeyAlias",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
);
SecretKey secretKey = keyGenerator.generateKey();
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
// AES鍵を生成
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(
new KeyGenParameterSpec.Builder("MyKeyAlias",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
);
SecretKey secretKey = keyGenerator.generateKey();
この鍵を使えば、アプリがパスワードを管理する必要がなくなります。
AndroidX の Security ライブラリを使うと、自動的に暗号化付きの SharedPreferences が使えます。
implementation "androidx.security:security-crypto:1.1.0-alpha06"
}
dependencies {
implementation "androidx.security:security-crypto:1.1.0-alpha06"
}
"secure_prefs",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
// データ保存
securePrefs.edit().putString("data", "暗号化される内容").apply();
// 読み込み
String decrypted = securePrefs.getString("data", null);
SharedPreferences securePrefs = EncryptedSharedPreferences.create(
"secure_prefs",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
// データ保存
securePrefs.edit().putString("data", "暗号化される内容").apply();
// 読み込み
String decrypted = securePrefs.getString("data", null);
- 簡単にやるなら EncryptedSharedPreferences(推奨)
- 自作したいなら AES暗号化(PBKDF2 + CBC/GCM)
- 高セキュリティなら Keystore + AES
簡単にやるなら EncryptedSharedPreferences(推奨)
自作したいなら AES暗号化(PBKDF2 + CBC/GCM)
高セキュリティなら Keystore + AES
👉 どの用途で使いたいですか?(例:ファイル保存 / SharedPreferences / DB保存) それによっておすすめの実装を絞れます。
あなた:
パスワード管理アプリを作りたい。ユーザー名とパスワードでログインして、パスワードを暗号化して保存したい
ChatGPT:
なるほど! 「パスワード管理アプリ」ですね。これはセキュリティが重要なので、 ユーザーのログイン管理 と 保存するパスワードの暗号化 をきちんと分けて考える必要があります。
- ユーザー登録(サインアップ)
ユーザー名 + パスワードを入力
入力パスワードはそのまま保存しない → ハッシュ化(PBKDF2, bcrypt, scrypt, Argon2 など)して保存 (ログインチェックのためだけに使う) 2. ユーザー名 + パスワードを入力 3. 入力パスワードはそのまま保存しない → ハッシュ化(PBKDF2, bcrypt, scrypt, Argon2 など)して保存 (ログインチェックのためだけに使う) 4. ログイン
入力されたパスワードを同じハッシュ関数に通して比較
OKならアプリを使用可能にする 5. 入力されたパスワードを同じハッシュ関数に通して比較 6. OKならアプリを使用可能にする 7. 保存するパスワード(ユーザーの各種サービス用パスワード)
ログイン時の「ユーザーパスワード」から暗号鍵を導出して AES で暗号化して保存
これにより、アプリのマスターパスワードを知らない限り データは復号できない 8. ログイン時の「ユーザーパスワード」から暗号鍵を導出して AES で暗号化して保存 9. これにより、アプリのマスターパスワードを知らない限り データは復号できない
ユーザー登録(サインアップ)
- ユーザー名 + パスワードを入力
- 入力パスワードはそのまま保存しない → ハッシュ化(PBKDF2, bcrypt, scrypt, Argon2 など)して保存 (ログインチェックのためだけに使う)
ユーザー名 + パスワードを入力
入力パスワードはそのまま保存しない → ハッシュ化(PBKDF2, bcrypt, scrypt, Argon2 など)して保存 (ログインチェックのためだけに使う)
ログイン
- 入力されたパスワードを同じハッシュ関数に通して比較
- OKならアプリを使用可能にする
入力されたパスワードを同じハッシュ関数に通して比較
OKならアプリを使用可能にする
保存するパスワード(ユーザーの各種サービス用パスワード)
- ログイン時の「ユーザーパスワード」から暗号鍵を導出して AES で暗号化して保存
- これにより、アプリのマスターパスワードを知らない限り データは復号できない
ログイン時の「ユーザーパスワード」から暗号鍵を導出して AES で暗号化して保存
これにより、アプリのマスターパスワードを知らない限り データは復号できない
ログイン認証用に使う(復号はできない)。
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
return Base64.encodeToString(tmp.getEncoded(), Base64.DEFAULT);
}
public static String hashPassword(String password, String salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
return Base64.encodeToString(tmp.getEncoded(), Base64.DEFAULT);
}
保存するときは例えば SQLite や SharedPreferences に以下を保存:
- username
- salt
- hashedPassword
username
salt
hashedPassword
ログイン時は入力されたパスワードを同じ方法でハッシュして比較する。
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), "AES");
}
public static String encryptPassword(String plainPassword, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(plainPassword.getBytes("UTF-8"));
// IVと暗号文を結合して保存
byte[] result = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
return Base64.encodeToString(result, Base64.DEFAULT);
}
public static String decryptPassword(String encryptedText, SecretKey key) throws Exception {
byte[] decoded = Base64.decode(encryptedText, Base64.DEFAULT);
byte[] iv = new byte[16];
System.arraycopy(decoded, 0, iv, 0, iv.length);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
byte[] encryptedBytes = new byte[decoded.length - iv.length];
System.arraycopy(decoded, iv.length, encryptedBytes, 0, encryptedBytes.length);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] original = cipher.doFinal(encryptedBytes);
return new String(original, "UTF-8");
}
public static SecretKey generateAESKey(String password, String salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), "AES");
}
public static String encryptPassword(String plainPassword, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(plainPassword.getBytes("UTF-8"));
// IVと暗号文を結合して保存
byte[] result = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
return Base64.encodeToString(result, Base64.DEFAULT);
}
public static String decryptPassword(String encryptedText, SecretKey key) throws Exception {
byte[] decoded = Base64.decode(encryptedText, Base64.DEFAULT);
byte[] iv = new byte[16];
System.arraycopy(decoded, 0, iv, 0, iv.length);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
byte[] encryptedBytes = new byte[decoded.length - iv.length];
System.arraycopy(decoded, iv.length, encryptedBytes, 0, encryptedBytes.length);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] original = cipher.doFinal(encryptedBytes);
return new String(original, "UTF-8");
}
| id | service | username | password_encrypted |
|---|---|---|---|
| 1 | Gmail | user1 | 暗号化済み文字列 |
| 2 | user2 | 暗号化済み文字列 |
- Keystore を使えば端末に依存した安全な鍵管理が可能 (ただしユーザーパスワード由来の暗号化との両立を工夫する必要あり)
- データベースを使う場合は SQLCipher で暗号化するとより安全
- バックアップをクラウドに置く場合は ゼロ知識方式(鍵はユーザーパスワードからのみ生成) を採用するとよい
Keystore を使えば端末に依存した安全な鍵管理が可能 (ただしユーザーパスワード由来の暗号化との両立を工夫する必要あり)
データベースを使う場合は SQLCipher で暗号化するとより安全
バックアップをクラウドに置く場合は ゼロ知識方式(鍵はユーザーパスワードからのみ生成) を採用するとよい
📌 まとめ
- ユーザーの ログインパスワードはハッシュ化して保存
- ユーザーパスワードを元にAES鍵を作って 保存するパスワードを暗号化
- SQLite や SharedPreferences に保存する
- 高度にするなら Keystore + SQLCipher を組み合わせる
ユーザーの ログインパスワードはハッシュ化して保存
ユーザーパスワードを元にAES鍵を作って 保存するパスワードを暗号化
SQLite や SharedPreferences に保存する
高度にするなら Keystore + SQLCipher を組み合わせる
👉 質問ですが、このパスワード管理アプリは 端末ローカル専用 ですか? それとも クラウド同期あり にしたいですか? 設計が変わってくるので、どちらを考えていますか?
あなた:
folderとitemは複数あるデータを読み込んで管理したい
<?xml version="1.0" encoding="shift-jis" standalone="yes"?>
ChatGPT:
なるほど、 XML構造のデータを読み込んで「フォルダとアイテム(folder/item)」を複数管理したい、ということですね。
Android Java でこれを扱うには、 主に次の3ステップで行うのが王道です。
XML 例(あなたのデータ):
<folder name="mail" open="true" selected="false">
<item name="yahoo" selected="false">
<account name="sample">@yahoo.co.jp</account>
<password name="Password">sample</password>
<url>yahoo</url>
<issueDate>11/21/2013</issueDate>
<expirationDate>11/21/2014</expirationDate>
</item>
<item name="gmail" selected="true">
<account name="example">@gmail.com</account>
<password name="Password">abc123</password>
<url>gmail</url>
</item>
</folder>
</idmData>
<idmData>
<folder name="mail" open="true" selected="false">
<item name="yahoo" selected="false">
<account name="sample">@yahoo.co.jp</account>
<password name="Password">sample</password>
<url>yahoo</url>
<issueDate>11/21/2013</issueDate>
<expirationDate>11/21/2014</expirationDate>
</item>
<item name="gmail" selected="true">
<account name="example">@gmail.com</account>
<password name="Password">abc123</password>
<url>gmail</url>
</item>
</folder>
</idmData>
public class Item {
public String name;
public String account;
public String password;
public String url;
public String issueDate;
public String expirationDate;
public Item(String name, String account, String password, String url,
String issueDate, String expirationDate) {
this.name = name;
this.account = account;
this.password = password;
this.url = url;
this.issueDate = issueDate;
this.expirationDate = expirationDate;
}
}
// Item.java
public class Item {
public String name;
public String account;
public String password;
public String url;
public String issueDate;
public String expirationDate;
public Item(String name, String account, String password, String url,
String issueDate, String expirationDate) {
this.name = name;
this.account = account;
this.password = password;
this.url = url;
this.issueDate = issueDate;
this.expirationDate = expirationDate;
}
}
import java.util.ArrayList;
import java.util.List;
public class Folder {
public String name;
public boolean open;
public boolean selected;
public List<Item> items = new ArrayList<>();
public Folder(String name, boolean open, boolean selected) {
this.name = name;
this.open = open;
this.selected = selected;
}
}
// Folder.java
import java.util.ArrayList;
import java.util.List;
public class Folder {
public String name;
public boolean open;
public boolean selected;
public List<Item> items = new ArrayList<>();
public Folder(String name, boolean open, boolean selected) {
this.name = name;
this.open = open;
this.selected = selected;
}
}
Android 標準の XmlPullParser を使うと、 外部ライブラリ不要で高速・安全に読み取れます。
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class XmlDataLoader {
public static List<Folder> load(Context context, InputStream inputStream) throws Exception {
List<Folder> folders = new ArrayList<>();
Folder currentFolder = null;
Item currentItem = null;
XmlPullParser parser = Xml.newPullParser();
parser.setInput(inputStream, "shift-jis");
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
String tagName = parser.getName();
switch (eventType) {
case XmlPullParser.START_TAG:
if ("folder".equals(tagName)) {
String name = parser.getAttributeValue(null, "name");
boolean open = Boolean.parseBoolean(parser.getAttributeValue(null, "open"));
boolean selected = Boolean.parseBoolean(parser.getAttributeValue(null, "selected"));
currentFolder = new Folder(name, open, selected);
} else if ("item".equals(tagName) && currentFolder != null) {
String name = parser.getAttributeValue(null, "name");
currentItem = new Item(name, "", "", "", "", "");
} else if (currentItem != null) {
if ("account".equals(tagName)) {
currentItem.account = parser.nextText();
} else if ("password".equals(tagName)) {
currentItem.password = parser.nextText();
} else if ("url".equals(tagName)) {
currentItem.url = parser.nextText();
} else if ("issueDate".equals(tagName)) {
currentItem.issueDate = parser.nextText();
} else if ("expirationDate".equals(tagName)) {
currentItem.expirationDate = parser.nextText();
}
}
break;
case XmlPullParser.END_TAG:
if ("item".equals(tagName) && currentFolder != null && currentItem != null) {
currentFolder.items.add(currentItem);
currentItem = null;
} else if ("folder".equals(tagName) && currentFolder != null) {
folders.add(currentFolder);
currentFolder = null;
}
break;
}
eventType = parser.next();
}
return folders;
}
}
import android.content.Context;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class XmlDataLoader {
public static List<Folder> load(Context context, InputStream inputStream) throws Exception {
List<Folder> folders = new ArrayList<>();
Folder currentFolder = null;
Item currentItem = null;
XmlPullParser parser = Xml.newPullParser();
parser.setInput(inputStream, "shift-jis");
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
String tagName = parser.getName();
switch (eventType) {
case XmlPullParser.START_TAG:
if ("folder".equals(tagName)) {
String name = parser.getAttributeValue(null, "name");
boolean open = Boolean.parseBoolean(parser.getAttributeValue(null, "open"));
boolean selected = Boolean.parseBoolean(parser.getAttributeValue(null, "selected"));
currentFolder = new Folder(name, open, selected);
} else if ("item".equals(tagName) && currentFolder != null) {
String name = parser.getAttributeValue(null, "name");
currentItem = new Item(name, "", "", "", "", "");
} else if (currentItem != null) {
if ("account".equals(tagName)) {
currentItem.account = parser.nextText();
} else if ("password".equals(tagName)) {
currentItem.password = parser.nextText();
} else if ("url".equals(tagName)) {
currentItem.url = parser.nextText();
} else if ("issueDate".equals(tagName)) {
currentItem.issueDate = parser.nextText();
} else if ("expirationDate".equals(tagName)) {
currentItem.expirationDate = parser.nextText();
}
}
break;
case XmlPullParser.END_TAG:
if ("item".equals(tagName) && currentFolder != null && currentItem != null) {
currentFolder.items.add(currentItem);
currentItem = null;
} else if ("folder".equals(tagName) && currentFolder != null) {
folders.add(currentFolder);
currentFolder = null;
}
break;
}
eventType = parser.next();
}
return folders;
}
}
List<Folder> folders = XmlDataLoader.load(this, is);
for (Folder folder : folders) {
Log.d("XML", "フォルダ: " + folder.name);
for (Item item : folder.items) {
Log.d("XML", " アイテム: " + item.name + ", アカウント=" + item.account + ", パス=" + item.password);
}
}
} catch (Exception e) {
e.printStackTrace();
}
try (InputStream is = getAssets().open("data.xml")) {
List<Folder> folders = XmlDataLoader.load(this, is);
for (Folder folder : folders) {
Log.d("XML", "フォルダ: " + folder.name);
for (Item item : folder.items) {
Log.d("XML", " アイテム: " + item.name + ", アカウント=" + item.account + ", パス=" + item.password);
}
}
} catch (Exception e) {
e.printStackTrace();
}
この password フィールドを保存・更新するときに 先ほど紹介した AES暗号化ユーティリティ(CryptoUtil) を使えば安全に保存できます。
例:
item.password = encrypted; String encrypted = CryptoUtil.encrypt(item.password, userMasterPassword, "someSalt"); item.password = encrypted;
読み込むとき:
String decrypted = CryptoUtil.decrypt(item.password, userMasterPassword, "someSalt");
| 目的 | 方法 |
|---|---|
| XMLデータを読み込む | XmlPullParser で Folder / Item にマッピング |
| フォルダ・アイテム管理 | List |
| 暗号化 | AES + ユーザーパスワードで暗号化/復号 |
| 文字コード | "shift-jis" を指定(日本語対応) |
👉 質問: このXMLは アプリ内のリソース(assets) から読み込む予定ですか? それとも 外部ストレージ / ダウンロードファイル などから読み込みたいですか? それによって読み込み方法を少し調整できます。
あなた:
ダウンロードファイルから読み込んで内部に保存して、バックアップとして出力できるようにしたい
ChatGPT:
完璧です — 目的がはっきりしましたね! あなたが作りたいのは、次のような 安全なパスワード管理アプリ構成 です👇
- ダウンロードフォルダのXMLファイルを選択して読み込む → Intent.ACTION_OPEN_DOCUMENT を使用(SAF: Storage Access Framework)
- アプリ内部ストレージ(context.getFilesDir())に保存する → 外部アクセス不可・アプリ専用領域
- アプリ内部で暗号化して保持・管理 → AES + ユーザーパスワード
- バックアップ出力(暗号化したXMLとして出力) → SAFを使って、ユーザーが出力先を選択
ダウンロードフォルダのXMLファイルを選択して読み込む → Intent.ACTION_OPEN_DOCUMENT を使用(SAF: Storage Access Framework)
アプリ内部ストレージ(context.getFilesDir())に保存する → 外部アクセス不可・アプリ専用領域
アプリ内部で暗号化して保持・管理 → AES + ユーザーパスワード
バックアップ出力(暗号化したXMLとして出力) → SAFを使って、ユーザーが出力先を選択
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/xml");
startActivityForResult(intent, 100);
// ファイル選択を開始
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/xml");
startActivityForResult(intent, 100);
結果を受け取る:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
try {
// InputStreamを取得
InputStream is = getContentResolver().openInputStream(uri);
// XMLをパースしてフォルダ・アイテム一覧にする
List<Folder> folders = XmlDataLoader.load(this, is);
// 内部ストレージに暗号化して保存
saveToInternalEncrypted(folders, "userPassword123");
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
try {
// InputStreamを取得
InputStream is = getContentResolver().openInputStream(uri);
// XMLをパースしてフォルダ・アイテム一覧にする
List<Folder> folders = XmlDataLoader.load(this, is);
// 内部ストレージに暗号化して保存
saveToInternalEncrypted(folders, "userPassword123");
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
内部保存用メソッド例:
// XML形式に戻す
StringBuilder xml = new StringBuilder();
xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<idmData>\n");
for (Folder folder : folders) {
xml.append("<folder name=\"").append(folder.name)
.append("\" open=\"").append(folder.open)
.append("\" selected=\"").append(folder.selected)
.append("\">\n");
for (Item item : folder.items) {
xml.append(" <item name=\"").append(item.name).append("\">\n");
xml.append(" <account>").append(item.account).append("</account>\n");
// パスワードを暗号化
String encryptedPassword = CryptoUtil.encrypt(item.password, masterPassword, "salt");
xml.append(" <password>").append(encryptedPassword).append("</password>\n");
xml.append(" <url>").append(item.url).append("</url>\n");
xml.append(" </item>\n");
}
xml.append("</folder>\n");
}
xml.append("</idmData>");
// 内部ストレージに保存
File file = new File(getFilesDir(), "data_encrypted.xml");
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(xml.toString().getBytes("UTF-8"));
}
}
private void saveToInternalEncrypted(List<Folder> folders, String masterPassword) throws Exception {
// XML形式に戻す
StringBuilder xml = new StringBuilder();
xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<idmData>\n");
for (Folder folder : folders) {
xml.append("<folder name=\"").append(folder.name)
.append("\" open=\"").append(folder.open)
.append("\" selected=\"").append(folder.selected)
.append("\">\n");
for (Item item : folder.items) {
xml.append(" <item name=\"").append(item.name).append("\">\n");
xml.append(" <account>").append(item.account).append("</account>\n");
// パスワードを暗号化
String encryptedPassword = CryptoUtil.encrypt(item.password, masterPassword, "salt");
xml.append(" <password>").append(encryptedPassword).append("</password>\n");
xml.append(" <url>").append(item.url).append("</url>\n");
xml.append(" </item>\n");
}
xml.append("</folder>\n");
}
xml.append("</idmData>");
// 内部ストレージに保存
File file = new File(getFilesDir(), "data_encrypted.xml");
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(xml.toString().getBytes("UTF-8"));
}
}
これで暗号化済みのデータが /data/data/<パッケージ名>/files/data_encrypted.xml に保存されます。
File file = new File(getFilesDir(), "data_encrypted.xml");
if (!file.exists()) return new ArrayList<>();
InputStream is = new FileInputStream(file);
List<Folder> folders = XmlDataLoader.load(this, is);
is.close();
// パスワードを復号化
for (Folder f : folders) {
for (Item i : f.items) {
i.password = CryptoUtil.decrypt(i.password, masterPassword, "salt");
}
}
return folders;
}
private List<Folder> loadFromInternalDecrypted(String masterPassword) throws Exception {
File file = new File(getFilesDir(), "data_encrypted.xml");
if (!file.exists()) return new ArrayList<>();
InputStream is = new FileInputStream(file);
List<Folder> folders = XmlDataLoader.load(this, is);
is.close();
// パスワードを復号化
for (Folder f : folders) {
for (Item i : f.items) {
i.password = CryptoUtil.decrypt(i.password, masterPassword, "salt");
}
}
return folders;
}
ユーザーに出力先を選ばせる:
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/xml");
intent.putExtra(Intent.EXTRA_TITLE, "backup.xml");
startActivityForResult(intent, 200);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/xml");
intent.putExtra(Intent.EXTRA_TITLE, "backup.xml");
startActivityForResult(intent, 200);
結果を受け取り:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 200 && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
try (OutputStream os = getContentResolver().openOutputStream(uri);
InputStream is = new FileInputStream(new File(getFilesDir(), "data_encrypted.xml"))) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 200 && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
try (OutputStream os = getContentResolver().openOutputStream(uri);
InputStream is = new FileInputStream(new File(getFilesDir(), "data_encrypted.xml"))) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
これでユーザーが指定した場所(例: Downloads, Google Drive)に 暗号化済みXMLをバックアップできます。
| 項目 | 説明 |
|---|---|
| 文字コード | Shift-JIS入力でもOK。出力はUTF-8推奨。 |
| 保存場所 | getFilesDir() 内 → 外部アプリからアクセス不可 |
| 安全性 | AES暗号化 + 内部ストレージで高セキュリティ |
| 復号キー | ユーザーのマスターパスワード(ログイン時)で再現可能 |
| UI設計 | 「インポート」「バックアップ」ボタンを設けると自然 |
もし希望があれば、 次のような 具体的なUI付き実装例(Activity) も提示できます:
- 「ファイルを読み込む」ボタン
- 「内部保存」自動暗号化
- 「バックアップ出力」ボタン
「ファイルを読み込む」ボタン
「内部保存」自動暗号化
「バックアップ出力」ボタン
👉質問: このアプリは「ログイン画面付き」にしますか?(マスターパスワード入力して入るタイプ) それとも単純にアプリ内でのみ暗号復号する想定ですか? → それにより 暗号鍵管理の仕組み を変えられます(Keystore利用など)。
あなた:
アプリ名を考えて
ChatGPT:
いいですね 😊 「パスワード管理+暗号化+バックアップ」が特徴のアプリですので、 セキュリティ・安心感・シンプルさを意識したネーミングが合います。
以下、いくつかタイプ別に提案します👇
| 日本語名 | 英語表記 | 意味・特徴 |
|---|---|---|
| セーフパス | SafePass | 安全にパスワードを守るイメージ。短く覚えやすい。 |
| パスセーフ | PassSafe | 「パスワード+セーフ」。Androidアプリ名に多い形。 |
| ロックノート | LockNote | 鍵付きノートのように扱う発想。 |
| マイキー | MyKey | 「自分の鍵」という個人的で安心感のある名前。 |
| セキュパス | SecuPass | “Security Password” の略風。響きが良く、軽快。 |
| 日本語名 | 英語表記 | 解説 |
|---|---|---|
| ガーディアンパス | GuardianPass | 守護者(Guardian)がパスワードを守る。 |
| シールドボックス | ShieldBox | シールド(盾)+ボックス(保管庫)。 |
| セキュアキー | SecureKey | 直感的で、プロっぽい響き。 |
| パスワードボルト | Password Vault | “Vault(金庫)” はパスワード管理アプリの定番語。 |
| キーセーフ | KeySafe | シンプルで堅実、UIにも合いやすい。 |
| 日本語名 | 英語表記 | 意味・雰囲気 |
|---|---|---|
| アークロック | ArcLock | “Arc(弧・橋渡し)”+“Lock”。未来的。 |
| ミモリ | Mimori | 「守り(まもり)」をもじった日本語風。柔らかい印象。 |
| しっかりパス | ShikkariPass | 優しい日本語UIに合う親しみやすさ。 |
| クロノロック | ChronoLock | “時間”や“期限管理”と連動するイメージにも。 |
| ハートロック | HeartLock | 個人的な大切な情報を守る感じ。 |
| 名前 | 特徴 |
|---|---|
| SafePass(セーフパス) | シンプル・国際的・安心感。Play Storeにも馴染む。 |
| GuardianPass(ガーディアンパス) | 守るイメージが強く、アプリの目的に合う。 |
| VaultNote(ボルトノート) | 金庫のようなノート。暗号化・バックアップとの相性◎。 |
👉もしアプリの デザイン方向(例:ビジネス向け / カジュアル / 和風) があれば、 そのトーンに合わせた名前候補をさらに出せます。
たとえば:
- ビジネス風 → 「SecureVault」「KeyManager」
- カジュアル風 → 「まもパス」「あんしんロック」
- 和風 / 個人向け → 「護り帳」「鍵の書」
ビジネス風 → 「SecureVault」「KeyManager」
カジュアル風 → 「まもパス」「あんしんロック」
和風 / 個人向け → 「護り帳」「鍵の書」
どんな方向性がいいですか? (例:「かっこいい系」「日本語で親しみやすい系」「海外でも通じる英語系」など)
あなた:
検索したら似たアプリが見つかったので被らない名前にして
ChatGPT:
了解です 👍 では、既存アプリと被らず、パスワード管理+暗号化+安全性+個人向けというイメージを踏まえて、 独自性のある候補をいくつか挙げますね。
- KagiNote(カギノート) 鍵+ノート。暗号化メモのような響き。
- HimitsuBox(ひみつボックス) 秘密をしまう箱。親しみやすく覚えやすい。
- Kakuho(確保) 「安全を確保する」意味合いで、セキュリティ系に合う。
- AnzenPass(安全パス) 安全でシンプル、パスワード管理アプリだとすぐわかる。
KagiNote(カギノート) 鍵+ノート。暗号化メモのような響き。
HimitsuBox(ひみつボックス) 秘密をしまう箱。親しみやすく覚えやすい。
Kakuho(確保) 「安全を確保する」意味合いで、セキュリティ系に合う。
AnzenPass(安全パス) 安全でシンプル、パスワード管理アプリだとすぐわかる。
- Lockfolio(Lock+Portfolio) 鍵の中にデータをまとめるイメージ。
- SafePassVault 「安全なパスワード金庫」。機能説明的でわかりやすい。
- Cryptiary(Crypt + Diary) 暗号化された日記のような響き。
- KeyLoom(鍵を織る) 安全な情報を織り上げる、独特な響きで被りにくい。
Lockfolio(Lock+Portfolio) 鍵の中にデータをまとめるイメージ。
SafePassVault 「安全なパスワード金庫」。機能説明的でわかりやすい。
Cryptiary(Crypt + Diary) 暗号化された日記のような響き。
KeyLoom(鍵を織る) 安全な情報を織り上げる、独特な響きで被りにくい。
- Passora(パソラ) — Password + Flora(花)で、「安全に咲く」印象。
- Kryptune(クリプチューン) — Crypto + Tune、「暗号化を整える」イメージ。
- Safion(セイフィオン) — Safe + Ion、「軽く安全なもの」という印象。
- Vaulton(ヴォールトン) — Vault(金庫)+on、「常に安全に」
Passora(パソラ) — Password + Flora(花)で、「安全に咲く」印象。
Kryptune(クリプチューン) — Crypto + Tune、「暗号化を整える」イメージ。
Safion(セイフィオン) — Safe + Ion、「軽く安全なもの」という印象。
Vaulton(ヴォールトン) — Vault(金庫)+on、「常に安全に」
ご希望に合わせて方向性を絞れます:
- 🔒 セキュリティ重視(例:SafePassVault, Lockfolio)
- 🪶 親しみやすさ重視(例:HimitsuBox, KagiNote)
- 💎 独自ブランド感(例:Cryptiary, Passora, Kryptune)
🔒 セキュリティ重視(例:SafePassVault, Lockfolio)
🪶 親しみやすさ重視(例:HimitsuBox, KagiNote)
💎 独自ブランド感(例:Cryptiary, Passora, Kryptune)
どんな雰囲気のアプリにしたいですか? (例:ビジネス向け・シンプル・かわいい・和風・未来的 など) それに合わせて、被らない名前をさらに絞り込みできます。