本文へ移動

CooLWJを検索

选择搜索范围,输入关键词后按回车。

言語

暗号化と復号の技法

一 対称鍵暗号

対称暗号と非対称暗号は異なる二つの暗号化方式で、データの安全性と情報の伝送において重要な役割を果たします。以下では、この二つの暗号技術をそれぞれ紹介します:

1.1 対称暗号(共通鍵暗号)

共通鍵暗号とは、暗号化と復号に同じ鍵を使う方式です。つまり、送信者と受信者の双方が、暗号化と復号の操作を行うためにこの鍵を持っていなければなりません。

一般的な共通鍵暗号アルゴリズム:

  • AES(高度暗号化標準)

  • DES(データ暗号化標準)

  • 3DES(トリプルデータ暗号化アルゴリズム)

1.2 対称暗号の特徴

一般に、共通鍵暗号には次のような特徴があります:

  1. 速度は比較的速い:暗号化と復号に同じ鍵を使用するため、共通鍵暗号は通常、公開鍵暗号より高速です。

  2. 鍵管理:対称暗号の主な課題は、鍵の配布と管理にある。鍵が漏洩すると、暗号化の安全性が脅かされる。

  3. 大量のデータに適している:高速であるため、共通鍵暗号は大量のデータの暗号化に適しています。

ここから、共通鍵暗号には主に二つの大きな利点があるとわかります。一つ目は演算速度が速いこと、二つ目は大量のデータに適していることです。

しかし、共通鍵暗号には致命的な問題が一つあります。それは鍵の管理です。サーバー側からクライアント側へ鍵を安全に送るにはどうすればよいかが問題になります!また、一対多の通信では、鍵が漏えいしないよう適切に管理することも課題です。これが共通鍵暗号の弱点です。

青緑色の壁とペイントローラーを描いたアート画像

1.3 コード例

続いて松哥が、Javaコードで共通鍵による暗号化と復号を行う方法を実演します。

Javaで対称暗号を実装する際は、通常、Java暗号化アーキテクチャ(Java Cryptography Architecture, JCA)が提供するクラスとインターフェースを使用します。

以下は、AES(高度暗号化標準)アルゴリズムを使って共通鍵暗号による暗号化と復号を行う簡単な例である:

public class SymmetricEncryptionExample {    // 生成密钥
    public static SecretKey generateKey() throws Exception {     
       KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128); // 可以是128, 192或256位
        return keyGenerator.generateKey();
    }    
    // 加密方法
    public static String encrypt(String data, SecretKey key) throws Exception {      
      Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);       
         byte[] encryptedBytes = cipher.doFinal(data.getBytes());        
         return Base64.getEncoder().encodeToString(encryptedBytes);
    }    
    // 解密方法
    public static String decrypt(String encryptedData, SecretKey key) throws Exception {        
    Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);       
         byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));       
          return new String(decryptedBytes);
    }    
    public static void main(String[] args) {        
    try {           
             // 生成密钥
            SecretKey key = generateKey();           
             // 原始数据
            String originalData = "Hello, JavaBoy!";            
            // 加密
            String encryptedData = encrypt(originalData, key);
            System.out.println("加密数据: " + encryptedData);            
            // 解密
            String decryptedData = decrypt(encryptedData, key);
            System.out.println("解密数据: " + decryptedData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 鍵の生成:使用する KeyGenerator AES鍵を生成します。

  2. 暗号化:使用する Cipher クラスを使って暗号化します。データをバイト列に変換してから暗号化し、Base64エンコードで文字列に変換することで、保存や転送をしやすくします。

  3. 復号:暗号化された文字列をバイト列にデコードし、同じ鍵を使って復号します。

上記のコードで注意すべき点は:

  • 鍵長(例えば 128 ビット)は、セキュリティ要件に応じて選ぶ必要がある。

  • 鍵を安全に保管し、コード内にハードコードしないでください。

  • 本番環境では、より安全な鍵管理方針の採用を検討すべきです。

セキュリティを考慮して、通常は上記の方法で鍵を生成します。この方法で生成される鍵には、システムを再起動するたびに変わるという特徴があります。鍵の生成を自分で制御したい場合は、次の方法で生成できます:

public static SecretKey generateKeyFromPassword(String password, int keySize) throws NoSuchAlgorithmException, InvalidKeySpecException {   
  SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");   
   KeySpec spec = new PBEKeySpec(password.toCharArray(), "salt".getBytes(), 65536, keySize);   
   return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
}

これで、自分で渡したパラメーターを通じて鍵を制御できます。

1.4 フロントエンドとバックエンドの組み合わせ

異なる言語(JavaScriptとJavaなど)の間でAESアルゴリズムによる暗号化と復号を行う場合、重要なのは、双方で同じ鍵、アルゴリズムのモード(CBC, ECBなど)、パディング方式(PKCS5Padding, PKCS7Paddingなど)、初期化ベクトル(IV、CBCなどIVを必要とするモードを使用する場合)を使用することです。

以前、フロントエンドで暗号化した後、バックエンドでどうしても復号できないという声があったので、ここで松哥もフロントエンドとバックエンドを組み合わせた例を紹介します。

フロントエンドで暗号化し、バックエンドで復号

フロントエンドのコード:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <title>AES加密示例</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
    </head>
    <body>
    <script>
    function encryptAES(text, secretKey) {        
    const key = CryptoJS.enc.Utf8.parse(secretKey);        
    const iv = CryptoJS.lib.WordArray.random(128 / 8); 
    // 对于CBC模式,需要IV
    
        const encrypted = CryptoJS.AES.encrypt(text, key, {           
         iv: iv,           
          mode: CryptoJS.mode.CBC,            
          padding: CryptoJS.pad.Pkcs7
        });        
        // 返回加密后的文本和IV(Base64格式),实际使用中可能需要安全地传输这些值
        return {          
          ciphertext: encrypted.toString(),          
            iv: iv.toString(CryptoJS.enc.Base64)
        };
    }    
    const secretKey = 'helloworldhelloworldhelloworld11'; /
    / 确保密钥是32个字符长(256位)
    const text = 'Hello, javaboy!';    
    const result = encryptAES(text, secretKey);    
    console.log(result);
    </script>
    </body>
    </html>


バックエンドのコード:

import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;public class AESDecrypt {    public static String decryptAES(String encryptedData, String secretKey, String iv) throws Exception {        IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);        byte[] decodedValue = Base64.getDecoder().decode(encryptedData);        byte[] decryptedValue = cipher.doFinal(decodedValue);        return new String(decryptedValue, "UTF-8");
    }    public static void main(String[] args) {        try {            //前端加密后的文本
            String encryptedText = "PYANpAjMsRnBIEhovtEXQw==";            String secretKey = "helloworldhelloworldhelloworld11";            //前端 IV,要和加密后的文本一起传到后端
            String iv = "y/jUHcgSOpOiyNlsfjNUBg==";            String decryptedText = decryptAES(encryptedText, secretKey, iv);
            System.out.println("解密文本: " + decryptedText);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ここでフロントエンドでの暗号化時にivパラメータが生成されることに注意しよう。これはフロントエンドの暗号化結果と一緒にバックエンドへ渡す必要がある。

バックエンドで暗号化し、フロントエンドで復号

JavaでAES暗号化を行い、JavaScriptで復号する場合も、両側で同じ鍵、アルゴリズムのモード(CBC、ECBなど)、パディング方式(PKCS5Padding、PKCS7Paddingなど)、そして(該当する場合は)同じ初期化ベクトル(IV)を使用する必要がある。

バックエンドのコード:

public class AESEncrypt {    public static String encryptAES(String plainText, String secretKey, String iv) throws Exception {        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(256); // 对于AES-256
        SecretKey secret = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");        IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secret, ivParameterSpec);        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));        return Base64.getEncoder().encodeToString(encrypted);
    }    public static void main(String[] args) {        try {            String plainText = "Hello, 江南一点雨!";            String secretKey = "helloworldhelloworldhelloworld11"; // 确保密钥是32个字符长(256位)
            String iv = Base64.getEncoder().encodeToString(new byte[16]); // 示例IV,实际应用中应更安全地生成

            String encryptedText = encryptAES(plainText, secretKey, iv);
            System.out.println("Encrypted text: " + encryptedText);
            System.out.println("IV (Base64): " + iv); // 确保将IV发送给解密方

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

フロントエンドのコード:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
    <script>
        function decryptAES(ciphertext, secretKey, iv) {            const key = CryptoJS.enc.Utf8.parse(secretKey);            const ivParsed = CryptoJS.enc.Base64.parse(iv);            const decrypted = CryptoJS.AES.decrypt(
                {                    ciphertext: CryptoJS.enc.Base64.parse(ciphertext)
                },
                key,
                {                    iv: ivParsed,                    mode: CryptoJS.mode.CBC,                    padding: CryptoJS.pad.Pkcs7
                }
            );            return decrypted.toString(CryptoJS.enc.Utf8);
        }        const secretKey = 'helloworldhelloworldhelloworld11';        const iv = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 从Java代码获取
        const ciphertext = '1lPNVF1injas78KUWeKp5FusEr6f0pGgcrLAg9ELFr8='; // 从Java代码获取

        const decryptedText = decryptAES(ciphertext, secretKey, iv);        console.log(decryptedText);    </script></head><body></body></html>

以上のフロントエンドとバックエンド間の通信における暗号化・復号コードは、松哥が自らテストし、いずれも問題なく動作しています。このような用途がある方は、参考にできるよう、この記事を忘れずにブックマークしておいてください。

二 非対称暗号

2.1 非対称暗号とは

非対称暗号は公開鍵暗号とも呼ばれ、二つの異なる鍵(公開鍵と秘密鍵)を使う暗号化方式である。

一般に、非対称暗号には次のような特徴がある:

  1. 公開鍵と秘密鍵

  • 公開鍵と秘密鍵の生成:非対称暗号では一対の鍵を使用します。公開鍵は公開されており、誰でもアクセスできますが、秘密鍵は非公開で、鍵の所有者だけがアクセスできます。この2つの鍵は数学的なアルゴリズムによって生成され、互いに関連していますが、一方から他方を導き出すことはできません。

  1. 高い安全性

  • 解読が困難:公開鍵と秘密鍵の間には複雑な数学的関係があるため、非対称暗号は高い安全性を備えています。攻撃者が公開鍵から秘密鍵を推測するのは非常に難しく、それによって暗号化されたデータの安全性が確保されます。

  • 量子計算による攻撃への耐性:一部のブロックチェーンプロジェクトでは、将来の量子計算の脅威に対処するため、楕円曲線デジタル署名アルゴリズム(ECDSA)の量子安全な派生方式など、量子計算による攻撃に耐性を持つ暗号アルゴリズムの採用が始まっています。

  1. 公開鍵を配布しやすい

  • 公開鍵の公開性:公開鍵は誰にでも公開できるため、配布が非常に容易です。誰でも公開鍵を使ってデータを暗号化できますが、復号できるのは秘密鍵の保有者だけです。

  1. 認証とデジタル署名

  • 本人認証:公開鍵はユーザーの識別子として使用でき、他者はその秘密鍵を知ることなく、ユーザーの本人確認を行える。これは、ブロックチェーンなどの分散型ネットワークで信頼できるアイデンティティを確立するのに役立つ。

  • デジタル署名:秘密鍵の所有者は秘密鍵を使ってメッセージに署名でき、ほかの人は公開鍵を使って署名の真正性を検証できます。これにより、メッセージの完全性と送信元の信頼性が保証されます。

  1. 安全な通信

  • 暗号化通信:公開鍵を使ってメッセージを暗号化でき、対応する秘密鍵を持つ人だけが復号できます。これにより、通信中のデータの安全性を確保し、権限のない人による情報へのアクセスを防ぎます。

  1. デジタル資産の管理

  • ブロックチェーンアドレスと秘密鍵:ブロックチェーンアドレスは通常、公開鍵から導出され、ユーザーは秘密鍵を使って、そのアドレスに関連付けられたデジタル資産を管理します。秘密鍵の安全性は、デジタル資産の安全にとって極めて重要です。

  1. 暗号化・復号の速度

  • 比較的低速:非対称暗号は、より複雑な数学的計算を必要とするため、暗号化と復号の速度が対称暗号に比べて大幅に遅くなります。ただし、このことは、安全な通信や本人認証などの分野での利用を妨げるものではありません。

2.2 非対称暗号の用途

非対称暗号には、代表的な利用場面が2つあります。

  1. 暗号化:最もよく知られている使い方で、公開鍵で暗号化し、秘密鍵で復号します。

  2. 署名:ネットワークは信頼できず、データは転送中に改ざんされる可能性があります。そのため、公開鍵と秘密鍵を逆の使い方で用い、秘密鍵でデータに署名し、公開鍵で署名を検証することで、データの安全性と完全性を確保します。

二つ目の用途については、秘密鍵で暗号化し、公開鍵で復号すると表現する人もいます。ともあれ、皆が同じことを指していると分かっていればよいでしょう。

非対称暗号アルゴリズムは、理論上はデータの暗号化にもデジタル署名にも利用できますが、実際には計算量の多さと効率の低さが大きな障害となっています。

対称暗号アルゴリズムと比べ、非対称暗号の演算速度は数桁遅く、大量のデータを処理する能力に大きく影響する。また、非対称暗号アルゴリズムの暗号化と復号の過程は鍵長と密接に関係し、ブロック暗号モードにも対応していないため、鍵長を超えない少量のデータしか処理できず、大量のデータを暗号化することはできない。

非対称暗号の性能上の弱点を補うため、現代の暗号システムでは通常、対称暗号と非対称暗号の長所を組み合わせたハイブリッド暗号方式を採用しています。この方式では、非対称暗号は主に、対称暗号の鍵を相手に安全に伝えるために使われます(いわゆる「鍵合意」)。双方がこの対称鍵を安全に共有した後は、効率のよい対称暗号アルゴリズムで大量のデータを暗号化・復号できます。このように組み合わせることで、暗号化の効率が向上するだけでなく、通信の安全性も高まるため、SSL/TLS などのさまざまな安全な通信プロトコルで広く使われています。

デジタル署名の分野でも、非対称暗号の効率を高めるための工夫が施されています。具体的には、まず元のデータにダイジェスト処理を行い(MD5、SHA などを利用できます)、固定長のダイジェスト値を得ます。次に、非対称暗号アルゴリズムでこのダイジェスト値を暗号化し、デジタル署名を生成します。ダイジェストアルゴリズムは任意の長さのデータを効率よく固定長のダイジェストに圧縮できるため、元のデータがどれほど大きくてもダイジェストの長さは同じで、署名処理も効率を維持できます。署名を検証する際は、元のデータのダイジェストを再計算し、復号した署名と比較するだけで、データが改ざんされたかどうかを素早く判断できます。

2.3 暗号化の例

Java コードで RSA による暗号化と復号を行う例:

public class RsaDemo {    public static void main(String[] args) {        try {            KeyPair keyPair = generateRSAKeyPair();            PublicKey publicKey = keyPair.getPublic();            PrivateKey privateKey = keyPair.getPrivate();            String originalData = "Hello, 江南一点雨!";            String encryptedData = encrypt(publicKey, originalData);            String decryptedData = decrypt(privateKey, encryptedData);

            System.out.println("加密后的数据: " + encryptedData);
            System.out.println("解密后的数据: " + decryptedData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }    public static String decrypt(PrivateKey privateKey, String encryptedData) throws Exception {        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);        byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));        return new String(decryptedBytes);
    }    public static String encrypt(PublicKey publicKey, String data) throws Exception {        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);        byte[] bytes = cipher.doFinal(data.getBytes());        return Base64.getEncoder().encodeToString(bytes);
    }    public static KeyPair generateRSAKeyPair() throws NoSuchAlgorithmException {        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048); // 可以指定密钥长度,如2048位
        return keyGen.generateKeyPair();
    }
}

2.4 フロントエンドとバックエンドの組み合わせ

フロントエンドで暗号化し、バックエンドで復号

バックエンドのコードは上の例と同じです。異なるのは、公開鍵を取得した後にそれを出力できる点です。この公開鍵は、後でフロントエンドに渡す必要があります:

public static void main(String[] args) {    try {        KeyPair keyPair = generateRSAKeyPair();        PublicKey publicKey = keyPair.getPublic();        PrivateKey privateKey = keyPair.getPrivate();
        System.out.println("公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));        String originalData = "Hello, 江南一点雨!";        String encryptedData = encrypt(publicKey, originalData);        String decryptedData = decrypt(privateKey, encryptedData);
        System.out.println("加密后的数据: " + encryptedData);
        System.out.println("解密后的数据: " + decryptedData);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

フロントエンドのコードは以下のとおり:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RSA Encryption Example</title>
    <!-- 引入jsencrypt库 -->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/bin/jsencrypt.min.js"></script></head><body><script>
    // 重写前端加密方法
    function encryptData(publicKey, data) {        // 创建一个新的JSEncrypt对象
        var encryptor = new JSEncrypt();        // 设置公钥
        encryptor.setPublicKey(publicKey);        // 加密数据
        var encrypted = encryptor.encrypt(data);        return encrypted;
    }    // 示例公钥(实际使用时应该替换为服务器提供的公钥)
    var publicKey = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnP5FOXIghidLDE2FgFwzi4Y+qTdYjnc0eMoiL5i4gdOeTE+7Uu9YvDB51GcBWKD9lvmZr5rX8z0OBpwe526qsTekNpXoIVk9DB34US0HOrAXEEwpUTVuSR656cJGAwmWBkVaalQynEz4Dlzrm53zExVYueruYUuzFyuZDaQcFnl3rWUH/XDkCIe23z0R1TfT3Q2OYNKft0u56r0S/ko99utXuYJK9yowe7QGT6q4cSJwsITQTomCARAq9q+bSNuGEYa4FlYCKIKWIhKbMhz0FYIMB2fJN10GyZbbvKASqeMkuCoD2Efgd8/6uMwOaMcx9LgEkcFaQ3qgDutsPXNUswIDAQAB`;    // 要加密的数据
    var data = 'Hello, javaboy!';    // 调用加密方法
    var encryptedData = encryptData(publicKey, data);    // 输出加密后的数据
    console.log('Encrypted Data:', encryptedData);</script></body></html>

バックエンドで暗号化し、フロントエンドで復号

JavaでRSAアルゴリズムを使ってデータを暗号化し、JavaScriptでそのデータを復号するということは、サーバー側がフロントエンドの公開鍵で暗号化し、フロントエンドが自分の秘密鍵で復号することを意味します。このような場合、フロントエンドの秘密鍵は盗まれやすいため、この使い方は推奨しません。そのため、例も挙げません。

さて、皆さんと改めて共通鍵暗号と公開鍵暗号についてお話ししました。上記のサンプルコードはすべて松哥がテストを通しているので、参考にしてください。


コメント欄

送信中

好きなテーマを選ぶ

選んだテーマはサイト全体に適用されます。