web-dev-qa-db-ja.com

機密データを暗号化してメモリに保存する

私はElectronic Funds Transfer(EFT)システムで働いており、クレジットカード番号などの機密データには細心の注意が必要です。

EFTゲートウェイに送信する前に、この情報でISO 8583データをマウントする必要があります。

Char []とbyte []を使用して機密データを保持し、この情報が不要になったときはArray.fillを使用します。

セキュリティを向上させるために考えることで、機密情報をラップするクラスを作成し、必要になるまでデータを暗号化しておくことが考えられます。

これは、ランダムに生成されたキーで使用する予定のコードです。

import Java.nio.ByteBuffer;
import Java.nio.CharBuffer;
import Java.nio.charset.Charset;
import Java.security.InvalidKeyException;
import Java.security.NoSuchAlgorithmException;
import Java.security.SecureRandom;
import Java.util.Arrays;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.security.auth.Destroyable;

public final class SensitiveChars implements CharSequence, Destroyable {

    private static final int KEYSIZE = 56;
    private static final String DES = "DES";
    private static final String DES_ECB_PKCS5_PADDING = DES + "/ECB/PKCS5Padding";

    private byte[] data;
    private Charset charset;
    private int lenght;

    private SecretKey secretKey;

    public SensitiveChars(char[] data) {
        super();
        this.charset = Charset.defaultCharset();
        ByteBuffer byteBuffer = charset.encode(CharBuffer.wrap(data));
        lenght = data.length;

        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance(DES);

            SecureRandom secureRandom = new SecureRandom();
            int keyBitSize = KEYSIZE;
            keyGenerator.init(keyBitSize, secureRandom);
            secretKey = keyGenerator.generateKey(); 

            setRawData(byteBuffer.array());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }

    public static SensitiveChars of(byte[] bytes) {
        return of(bytes, Charset.defaultCharset());
    }   

    public static SensitiveChars of(byte[] bytes, Charset charset) {
        return of(bytes, charset, false);
    }

    public static SensitiveChars of(byte[] bytes, Charset charset, boolean cleanBytes) {
        char[] chars = toChar(bytes, charset, cleanBytes);
        return new SensitiveChars(chars);
    }

    @Override
    public int length() {
        return lenght;
    }

    @Override
    public char charAt(int index) {
        char[] tempArray = getChars();
        char c = tempArray[index];
        clearData(tempArray);
        return c;
    }

    @Override
    public CharSequence subSequence(int start, int end) {
        char[] dataTmp = getChars();
        char[] range = Arrays.copyOfRange(dataTmp, start, end);
        clearData(dataTmp);
        return new SensitiveChars(range);
    }

    public char[] getChars() {
        if (lenght == 0) {
            return new char[0];
        } else {
            byte[] rawData = getRawData();
            ByteBuffer byteBuffer = ByteBuffer.wrap(rawData);
            CharBuffer charBuffer = charset.decode(byteBuffer);
            char[] result = Arrays.copyOf(charBuffer.array(), lenght);
            clearData(charBuffer.array());
            clearData(rawData);
            return result;
        }
    }

    public void append(char[] chars) {
        char[] dataTmp = getChars();
        int lengthData = dataTmp.length;
        char[] newData = Arrays.copyOf(dataTmp, lengthData + chars.length);
        clearData(data);

        int posIni = lengthData;
        for (int i = 0; i < chars.length; i++) {
            newData[i + posIni] = chars[i];
        }

        CharBuffer charBuffer = CharBuffer.wrap(newData);
        ByteBuffer byteBuffer = charset.encode(charBuffer);
        byteBuffer.compact();
        setRawData(byteBuffer.array());
        lenght = newData.length;

        clearData(dataTmp);
        clearData(newData);
    }

    private void setRawData(byte[] data) {
        this.data = encrypt(data);
    }

    private byte[] getRawData() {
        return decrypt(data);
    }

    @Override
    public void destroy() {
        clearData(data);
        data = new byte[0];
        lenght = 0;
    }

    private byte[] encrypt(byte[] bytes) {
        try {
            Cipher cipher = Cipher.getInstance(DES_ECB_PKCS5_PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            return cipher.doFinal(bytes);
        } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException
                | BadPaddingException e) {
            throw new IllegalStateException(e);
        }
    }

    private byte[] decrypt(byte[] bytes) {
        try {
            Cipher cipher = Cipher.getInstance(DES_ECB_PKCS5_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
            return cipher.doFinal(bytes);
        } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException
                | BadPaddingException e) {
            throw new IllegalStateException(e);
        }
    }   

    private static void clearData(byte[] cs) {
        Arrays.fill(cs, (byte) 0); 
    }   

    private static void clearData(char[] cs) {
        CharsUtils.clearData(cs);
    }       

}

それは良いアイデアですか、それとも別のアプローチを使用する必要がありますか?

編集1:アプリケーションは https://www.pcisecuritystandards.org/ に準拠する必要があります

1
Rodrigo Menezes

セキュリティについて話すときは、心配していることを明確に定義することが重要です。これは通常「脅威モデル」と呼ばれます。アプリケーションのメモリをキャプチャし、そこからカード番号を取得する誰かに関係していると聞きました。

それが正しい場合、あなたの鍵も同じメモリ空間とそれを復号化するコードに格納されているという根本的な問題があると思います。データを取得するのが少し難しくなるだけです。これは、基本的な難読化手法と同じだと判断します。

余談ですが、DESは廃止された暗号です。このトピックに興味がある場合は、さらに調査を行うことをお勧めします。最新の状態に保つことも重要です。古い記事では、当時権威があったとしても安全とは見なされていないアプローチの使用を提案している可能性があります。

8
JimmyJames

OK、あなたの脅威モデルは何ですか?

  • メモリが過負荷になると、データはページファイルに到達し、他のプロセスがページファイルをピークします。このソリューションは、ページファイルに復号化されたページを入れないようにオッズを改善しますが、RAMのどこかでデータを復号化する必要があるため、そのような可能性を排除しません。正しい解決策は、ディスクへのページングがまったくないか、暗号化されたページファイル/スワップパーティションです。

  • ルージュプロセスは、システムの制御を十分にキャプチャして、他のプロセスのRAMを直接監視できます。たとえば、管理者権限を取得するなどです。これは、このボックスのすべてのプロセスに対する一種のゲームオーバーです。ルージュプロセスは、 RAMまたはDB内で、復号化キーを格納し、保護された情報を復号化します。ソリューションは無関係になります。

  • サーバー内で完全に信頼されていないコードを実行します。ある種のサードパーティのプラグインで、アクセスできないはずの情報を覗き見させたくない場合。上記を参照。

正しい解決策は、明確な特権の分離に基づいて、コードに必要な最小限の特権を与え、信頼できるコードの量をできるだけ少なくすることです。

たとえば、メインアプリケーションの機密データは常に暗号化しておきます。RAMにフェッチされることはありません。それはおそらくその存在をチェックすることを除いて、それとは何の関係もありません。このプロセスは、復号化キーにアクセスできませんまったくデータベースを実行するマシンは、復号化キーにもアクセスできません。

エクスポートのプロセスを別のプロセスとして、または別のVMとして、あるいは別の物理ボックスとしても実行します。そのプロセスは、エクスポートするレコードのIDを取得し、それらをデータベース(暗号化されて格納されている場所)からフェッチし、それらを復号化して、おそらく別の暗号化チャネルを使用して受信側に送信する非常に小さなコードです(例: TLS接続経由、または暗号化ファイルの書き込み)。

エクスポーターだけが、おそらく他の場所に格納されている復号化キーにアクセスでき、データベースの関連部分を読み取り(書き込みではなく)、エクスポートチャネルに出力するのに十分な特権を持っている必要があります。おそらく、秘密鍵の保存、アクセス、ローテーションのためのインフラストラクチャがすでにあるはずです。エクスポーターはできるだけ短い時間実行し、使用されていないときはシャットダウンする必要があります。確かに、インターネットからはアクセスできず、アプリケーションの他の部分を実行しているマシンを含め、内部ネットワークからは最低限アクセスできる必要があります。

これにより、リモートの攻撃対象領域が減り、攻撃者が1層または2層追加して、たとえばメインアプリサーバーまたはデータベースサーバーが危険にさらされています。

4
9000