【AES】crypto-js と PyCrypto の互換性

crypto-jsPyCryptoのAES暗号(CFBモード)の互換性のための拡張について書きます。

ブロック暗号のモードは, ECB, CBC, CFB, OFBなどがあり, ECBについては暗号の危殆化により使用が非推奨になっています。

基本アイデアはこちらを参考にしました。

crypto-js

crypto-jsをインストールする。今回の例はバックエンドですが, フロントエンドでもほぼ同様です。

$ npm install crypto-js -g

CryptoJSに MODE_CFB を追加する。

var CryptoJS = require("crypto-js");

/**
 * Cipher Feedback block mode.
 */
CryptoJS.mode.CFB = (function () {
    var CFB = CryptoJS.lib.BlockCipherMode.extend();

    CFB.Encryptor = CFB.extend({
        processBlock: function (words, offset) {
            // Shortcuts
            var cipher = this._cipher;
            var blockSize = cipher.blockSize;

            generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher);

            // Remember this block to use with next block
            this._prevBlock = words.slice(offset, offset + blockSize);
        }
    });

    CFB.Decryptor = CFB.extend({
        processBlock: function (words, offset) {
            // Shortcuts
            var cipher = this._cipher;
            var blockSize = cipher.blockSize;

            // Remember this block to use with next block
            var thisBlock = words.slice(offset, offset + blockSize);

            generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher);

            // This block becomes the previous block
            this._prevBlock = thisBlock;
        }
    });

    function generateKeystreamAndEncrypt(words, offset, blockSize, cipher) {
        // Shortcut
        var iv = this._iv;

        // Generate keystream
        if (iv) {
            var keystream = iv.slice(0);

            // Remove IV for subsequent blocks
            this._iv = undefined;
        } else {
            var keystream = this._prevBlock;
        }
        cipher.encryptBlock(keystream, 0);

        // Encrypt
        for (var i = 0; i < blockSize; i++) {
            words[offset + i] ^= keystream[i];
        }
    }

    return CFB;
}());

module.exports = CryptoJS;

require()で読み込む。key と iv は hex なので ascii に変換してからAESに入れている。

'use strict';

var AES = require("crypto-js/aes");
var CryptoJS = require("./crypto-js/cfb.js")

function aesEncryptModeCFB (msg, key, iv) {
    return AES.encrypt(msg, key, {iv: iv, mode: CryptoJS.mode.CFB}).toString()
}

function aesDecryptModeCFB (cipher, key, iv) {
    return AES.decrypt(cipher, key, {iv: iv, mode: CryptoJS.mode.CFB}).toString(CryptoJS.enc.Utf8);
}

var encrypted = {
    "key": '01ab38d5e05c92aa098921d9d4626107133c7e2ab0e4849558921ebcc242bcb0',
    "iv": '6aa60df8ff95955ec605d5689036ee88',
    "ciphertext": 'r19YcF8gc8bgk5NNui6I3w=='
}

var key = CryptoJS.enc.Hex.parse(encrypted.key)
var iv = CryptoJS.enc.Hex.parse(encrypted.iv)
var cipher = CryptoJS.lib.CipherParams.create({
    ciphertext: CryptoJS.enc.Base64.parse(encrypted.ciphertext)
})

console.log(aesDecryptModeCFB(cipher, key, iv))
// => fisproject
console.log(aesEncryptModeCFB('fisproject', key, iv))
// => r19YcF8gc8bgk5NNui6I3w==

PyCrypto

Python側では Padding 処理を追加する。

# pip install -U pip
$ sudo pip install pycrypto

crypt-jsは Padding に PKCS#7(rfc2315)を使っているので PyCrypto で暗号・復号時にこのPadding処理を追加する。

#!/usr/bin/env python
# coding: utf-8
  
import os, json
import binascii
from Crypto import Random
from Crypto.Cipher import AES
 
# ------------------------------
# DEFINE Encryption Class
class Cryptor(object):
    # AES-256 key (32 bytes)
    KEY = "01ab38d5e05c92aa098921d9d4626107133c7e2ab0e4849558921ebcc242bcb0"
    BLOCK_SIZE = 16
    
    @classmethod
    def _pad_string(cls, in_string):
        '''Pad an input string according to PKCS#7'''
        in_len = len(in_string)
        pad_size = cls.BLOCK_SIZE - (in_len % cls.BLOCK_SIZE)
        return in_string.ljust(in_len + pad_size, chr(pad_size))
    
    @classmethod
    def _unpad_string(cls, in_string):
        '''Remove the PKCS#7 padding from a text string'''
        in_len = len(in_string)
        pad_size = ord(in_string[-1])
        if pad_size > cls.BLOCK_SIZE:
            raise ValueError('Input is not padded or padding is corrupt')
        return in_string[:in_len - pad_size]
    
    @classmethod
    def generate_iv(cls, size=16):
        return Random.get_random_bytes(size)
    
    @classmethod
    def encrypt(cls, in_string, in_key, in_iv=None):

        key = binascii.a2b_hex(in_key)
        
        if in_iv is None:
            iv = cls.generate_iv()
            in_iv = binascii.b2a_hex(iv)
        else:
            iv = binascii.a2b_hex(in_iv)
        
        aes = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
        return in_iv, aes.encrypt(cls._pad_string(in_string))
    
    @classmethod
    def decrypt(cls, in_encrypted, in_key, in_iv):

        key = binascii.a2b_hex(in_key)
        iv = binascii.a2b_hex(in_iv)
        aes = AES.new(key, AES.MODE_CFB, iv, segment_size=128)      
        
        decrypted = aes.decrypt(binascii.a2b_base64(in_encrypted).rstrip())
        return cls._unpad_string(decrypted)

上記をimportして使う。

#!/usr/bin/env python
# coding: utf-8

from AES_Compatible_Cryptjs import Cryptor
import binascii

iv, encrypted = Cryptor.encrypt('fisproject', Cryptor.KEY)

print "iv : %s" % iv
# => iv  6aa60df8ff95955ec605d5689036ee88
print "encrypted : %s" % binascii.b2a_base64(encrypted).rstrip()
# => encrypted  r19YcF8gc8bgk5NNui6I3w==

decrypted = Cryptor.decrypt('r19YcF8gc8bgk5NNui6I3w==', Cryptor.KEY, '6aa60df8ff95955ec605d5689036ee88')
print "decrypted : %s" % decrypted
# => decrypted : fisproject

codeはGitHubにあります。