KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

パスワード管理アプリを作りたい KagiNote

 パソコンで管理しているパスワードをスマホでもバックアップしたい。ただパスワードをテキストのまま保存するのはセキュリティー的にどうかと思うので暗号化して保存したい。

パソコンでのパスワードの保存はchromeにまかせていたり、バックアップにIDmanagerを使っている。ただandroid版がないのでスマホで管理ができない。アンドロイドアプリのパスワードマネージャーはいくらでもあるが、google以外だとどうも信用できないのと。Googleアカウント紛失時のバックアップとして使いたい。自分で作るなら、IDmanagerのXMLを読み込んでアプリ内部に暗号化して保持したい。

www.woodensoldier.info

あなた:

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:

なるほど! 「パスワード管理アプリ」ですね。これはセキュリティが重要なので、 ユーザーのログイン管理 と 保存するパスワードの暗号化 をきちんと分けて考える必要があります。

  1. ユーザー登録(サインアップ)

ユーザー名 + パスワードを入力

入力パスワードはそのまま保存しない → ハッシュ化(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 Twitter 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"?> <e-mail1></e-mail1> <e-mail2></e-mail2> <e-mail3></e-mail3> Account ID Password @yahoo.co.jp sample yahoo <e-mail></e-mail> 11/21/2013 11/21/2014 0

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 と List を使う
暗号化 AES + ユーザーパスワードで暗号化/復号
文字コード "shift-jis" を指定(日本語対応)

👉 質問: このXMLは アプリ内のリソース(assets) から読み込む予定ですか? それとも 外部ストレージ / ダウンロードファイル などから読み込みたいですか? それによって読み込み方法を少し調整できます。

あなた:

ダウンロードファイルから読み込んで内部に保存して、バックアップとして出力できるようにしたい

ChatGPT:

完璧です — 目的がはっきりしましたね! あなたが作りたいのは、次のような 安全なパスワード管理アプリ構成 です👇

  1. ダウンロードフォルダのXMLファイルを選択して読み込む  → Intent.ACTION_OPEN_DOCUMENT を使用(SAF: Storage Access Framework)
  2. アプリ内部ストレージ(context.getFilesDir())に保存する  → 外部アクセス不可・アプリ専用領域
  3. アプリ内部で暗号化して保持・管理  → AES + ユーザーパスワード
  4. バックアップ出力(暗号化した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:

了解です 👍 では、既存アプリと被らず、パスワード管理+暗号化+安全性+個人向けというイメージを踏まえて、 独自性のある候補をいくつか挙げますね。

  1. KagiNote(カギノート)  鍵+ノート。暗号化メモのような響き。
  2. HimitsuBox(ひみつボックス)  秘密をしまう箱。親しみやすく覚えやすい。
  3. Kakuho(確保)  「安全を確保する」意味合いで、セキュリティ系に合う。
  4. AnzenPass(安全パス)  安全でシンプル、パスワード管理アプリだとすぐわかる。

KagiNote(カギノート)  鍵+ノート。暗号化メモのような響き。

HimitsuBox(ひみつボックス)  秘密をしまう箱。親しみやすく覚えやすい。

Kakuho(確保)  「安全を確保する」意味合いで、セキュリティ系に合う。

AnzenPass(安全パス)  安全でシンプル、パスワード管理アプリだとすぐわかる。

  1. Lockfolio(Lock+Portfolio)  鍵の中にデータをまとめるイメージ。
  2. SafePassVault  「安全なパスワード金庫」。機能説明的でわかりやすい。
  3. Cryptiary(Crypt + Diary)  暗号化された日記のような響き。
  4. KeyLoom(鍵を織る)  安全な情報を織り上げる、独特な響きで被りにくい。

Lockfolio(Lock+Portfolio)  鍵の中にデータをまとめるイメージ。

SafePassVault  「安全なパスワード金庫」。機能説明的でわかりやすい。

Cryptiary(Crypt + Diary)  暗号化された日記のような響き。

KeyLoom(鍵を織る)  安全な情報を織り上げる、独特な響きで被りにくい。

  1. Passora(パソラ) — Password + Flora(花)で、「安全に咲く」印象。
  2. Kryptune(クリプチューン) — Crypto + Tune、「暗号化を整える」イメージ。
  3. Safion(セイフィオン) — Safe + Ion、「軽く安全なもの」という印象。
  4. 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)

どんな雰囲気のアプリにしたいですか? (例:ビジネス向け・シンプル・かわいい・和風・未来的 など) それに合わせて、被らない名前をさらに絞り込みできます。