Base64 かどうか判定する

Apache commons の codec https://commons.apache.org/proper/commons-codec/
に存在する Base64 かどうかを判定する処理、
他の処理は commons の codec にあるものを使わなくてもいいから、
とにかく Base64 かどうかを判定する処理だけが欲しかった。
よって、ソースから持ってくる。
ソースを見たところこれ以上速くてパフォーマンスが良いコードは無さそうだ。

/**
 * B64Util : Base64 判定を提供
 */
public final class B64Util{
   private static final byte[] DECODE_TABLE = {
   //   0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, // 20-2f + - /
       52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 30-3f 0-9
       -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, // 40-4f A-O
       15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, // 50-5f P-Z _
       -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o
       41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51                      // 70-7a p-z
   };
   private static Pattern b64pattern = Pattern.compile("^[a-z0-9A-Z/\\+]+={1,2}$");
   private B64Util(){}
   /**
    * byte[] Base64 判定.
    * @param array 対象 byte[]
    * @return true=Base64 の byte[]
    */
   public static boolean isBase64(byte[] array){
      if (array==null) return false;
      if (array.length < 1) return false;
      for(int i=0; i < array.length; i++){
         if (!isBase64(array[i]) && !isWhiteSpace(array[i])){
            return false;
         }
      }
      return true;
   }
   /**
    * String Base64 判定.
    * @param str 対象 String
    * @return true=Base64文字列
    */
   public static boolean isBase64(String str){
      if (str==null) return false;
      return isBase64(str.getBytes()) && b64pattern.matcher(str).matches();
   }
   private static boolean isBase64(byte octet){
      return octet == '=' || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1);
   }
   private static boolean isWhiteSpace(byte byteToCheck){
      switch(byteToCheck){
         case ' ' :
         case '\n' :
         case '\r' :
         case '\t' :
            return true;
         default :
            return false;
      }
   }
}

持ってきたものに、正規表現チェックを追加した
「こういうので、いいんですよ。。」

Java と Python 相互で AES 256 暗号/複合を実行する

本記事よりも、2019-3-2 に書き改めた方を参照すべし。。
oboe2uran.hatenablog.com



試したのは、CBCモードとECBモードです。
Python暗号→Java複合は問題ないのですが、Base64 エンコードで相互受け渡すとして
Java暗号→Python複合では、暗号化した文字列長さが4の倍数になっても、
暗号文を Base64 b64decode 実行した時に、
  binascii.Error: Incorrect padding
が発生してしまいます。

解決した方法は、Java暗号→Python複合の受け渡しを Base64 エンコードではなくて
別の表現、16進数 HEX表現文字列を渡すことにしました。

Java 側の準備
クラス内で、private 変数で以下を用意します。

private SecretKeySpec key;
private IvParameterSpec iv;
private byte[] ivary;
private String mode;

コンストラクタなどで、共通鍵になる String password
イニシャライズベクトル値 byte[] ivector を受け取って準備します。
以下は、CBCモードです。ECBモードなら、ベクトル値は不要

try{
   byte[] keydata = password.getBytes();
   MessageDigest sha = MessageDigest.getInstance("SHA-256");
   keydata = sha.digest(keydata);
   keydata = Arrays.copyOf(keydata, 32);
   key = new SecretKeySpec(keydata, "AES");
   mode = "AES/CBC/PKCS5Padding";
   ivary = Arrays.copyOf(ivector, 16);
   iv = new IvParameterSpec(ivary);
}catch(NoSuchAlgorithmException e){
   throw new RuntimeException(e.getMessage());
}

CBCモード 暗号化

public byte[] encrypt(String message){
   try{
      Cipher cipher = Cipher.getInstance(mode);
      cipher.init(Cipher.ENCRYPT_MODE, key, iv);
      return cipher.doFinal(message.getBytes());
   }catch(Exception e){
      throw new RuntimeException(e);
   }
}

CBCモード 複合化

public byte[] decrypt(String encryptText){
   try{
      Cipher cipher = Cipher.getInstance(mode);
      cipher.init(Cipher.DECRYPT_MODE, key, iv);
      byte[] cipherBytes = Base64.getDecoder().decode(encryptText);
      return cipher.doFinal(cipherBytes);
   }catch(Exception e){
      throw new RuntimeException(e);
   }
}

ベクトル値の決定は状況によっていろんな方法あると思いますが、
例えば、16進数 HEX表現文字列から求めるなら、

List<Byte> listb = Pattern.compile("[\\w]{1,2}").matcher(ivhex)
   .results().map(r->(byte)Integer.parseInt(r.group(), 16))
   .collect(Collectors.toList());
byte[] iv = Bytes.toArray(listb);

でも良いと思います。

Pythonで暗号→ PythonBase64エンコードして受信してJavaでの複合、
Python暗号→Java複合

String plane 
= new String(Base64.getDecoder().decode(decrypt(encedtxt)), StandardCharsets.UTF_8);

Java暗号→Java複合

String plane 
= new String(decode(decrypt(encedtxt)), StandardCharsets.UTF_8);

この差があります。

Java暗号→Python複合では、
Javaで暗号、Base64エンコードして渡したいのですが、それだと
Python側で Base64 b64decode 処理でエラーになるので、
以下のように、暗号化したら、16進数 HEX表現文字列にして渡すことにします。

byte[] encdat = cipher.encrypt(planetxt);

String hexdata = IntStream.range(0, encdat.length)
   .mapToObj(i->String.format("%02x", encdat[i]))
   .collect(Collectors.joining(""));

Python

Pythonソース名、cryptoaes256.py とか、名付けてます。
暗号メソッド、複合メソッド、CBCモード、ECBモードごとに分けるのもどうかと
思ったのですが、コンストラクタ生成で決めるのも引数多くて嫌だし、
メソッドに引数でモード指定するのも嫌だし
かっこ悪くてもメソッド名で区別することにしました。

→ 考え直し、書き直す!!
他言語と相互間の暗合複合を考慮の AES 暗合複合 Python のコード - Oboe吹きプログラマの黙示録
以下は、それまでの、パディングが、JavaPython の差
をどう吸収させるか苦労した形跡。。。

問題の Java で暗号化したものを 16進 Hex表現で受け取った時の複合だけ
decryptCBCbin decryptECBbin というメソッドを呼び出します。

# -*- coding: UTF-8 -*-
import re
import hashlib
from Crypto.Cipher import AES
from Crypto import Random
from base64 import b64encode, b64decode

BLOCK_SIZE = AES.block_size
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
#unpad = lambda s: s[:-ord(s[len(s) - 1:])]  # 不使用

class AESCipher:
    def __init__(self, password, iv=Random.new().read(AES.block_size)):
        self.key = hashlib.sha256(password.encode("utf-8")).digest()
        self.iv = iv
    # ベクトル値取得
    def getIV(self):
        return self.iv
    # password & ベクトル値セット
    def setPasswdAndIV(self, passandiv):
        self.key = hashlib.sha256(passandiv[0].encode("utf-8")).digest()
        self.iv = passandiv[1]
    # AES 256キー参照
    def getKey(self):
        return self.key

    # CBC モード暗号化  plane → byte
    def encryptCBC(self, message):
        if message is None or len(message) == 0:
            raise NameError("No value given to encrypt")
        raw = b64encode(message.encode('utf-8')).decode()
        raw = pad(raw)
        raw = raw.encode('utf-8')
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        return cipher.encrypt(raw)

    # ECB モード暗号化  plane → byte
    def encryptECB(self, message):
        if message is None or len(message) == 0:
            raise NameError("No value given to encrypt")
        raw = b64encode(message.encode('utf-8')).decode()
        raw = pad(raw)
        raw = raw.encode('utf-8')
        cipher = AES.new(self.key, AES.MODE_ECB)
        return cipher.encrypt(raw)

    # CBC モード複合  encrypted  base64 → plane
    def decryptCBC(self, enctext):
        if enctext is None or len(enctext) == 0:
            raise NameError("No value given to decrypt")
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        return b64decode(cipher.decrypt(b64decode(enctext))).decode()

    # CBC モード複合  encrypted  HEX express binary → plane
    def decryptCBCbin(self, encbinary):
        if encbinary is None or len(encbinary) == 0:
            raise NameError("No value given to decrypt")
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        return re.sub(b'\x08*$', b'', cipher.decrypt(encbinary)).decode()

    # ECB モード複合  encrypted  base64 → plane
    def decryptECB(self, enctxt):
        if enctxt is None or len(enctxt) == 0:
            raise NameError("No value given to decrypt")
        cipher = AES.new(self.key, AES.MODE_ECB)
        return b64decode(cipher.decrypt(b64decode(enctxt)).decode()).decode()

    # ECB モード複合  encrypted  HEX express binary → plane
    def decryptECBbin(self, encbinary):
        if encbinary is None or len(encbinary) == 0:
            raise NameError("No value given to decrypt")
        cipher = AES.new(self.key, AES.MODE_ECB)
        return re.sub(b'\x08*$', b'', cipher.decrypt(encbinary)).decode()

# password と iv を再セットするための タプルを生成
def createPasswordAndIV():
    import random
    import string
    return  (''.join(random.choices(string.ascii_letters + string.digits, k=16)), Random.new().read(AES.block_size) )

decryptCBCbin と decryptECBbin

return re.sub(b'\x08*$', b'', cipher.decrypt(encbinary)).decode()

ではなくて、最後の \x00 を置換するように

return re.sub(b'\x00*$', b'', cipher.decrypt(encbinary)).decode()

ではないかとも思ったのですが、実行してみると、 \x08 にしないとダメでした。
Python での暗号

from cryptoaes256 import AESCipher
from base64 import b64encode

# 鍵や vector は適当に持ってくる
aes = AESCipher(key, bytes.fromhex(ivhex))
enctxt = b64encode(aes.encryptCBC(planetxt)).decode('utf-8')

Python で暗号したものを複合

aes = AESCipher(key, bytes.fromhex(ivhex))
planetxt = aes.decryptCBC(enctext)

Javaで暗号化→16進 Hex文字列を受け取って、複合

from cryptoaes256 import AESCipher

aes = AESCipher(key, bytes.fromhex(ivhex))
planetxt = aes.decryptCBCbin(bytes.fromhex(enchexstring))

Java 16進数文字列から、byte[]

Java 16進数文字列から、2文字ずつ切り取って byte[] を作る。
必ず2文字ずつ切り出せる文字列であるとする。=すなわち長さが2で割り切れる。

String hexstring= "56d19eaf";

クラシックな方法

Matcher m = Pattern.compile("[\\w]{1,2}").matcher(hexstring);
while(m.find()){
   // TODO m.group() からbyte[] に格納する
}

Java9 以降の Stream

Pattern.compile("[\\w]{1,2}").matcher(hexstring).results().forEach(r->{
   // TODO  r.group() から byte[] に格納する
});

Java9 以降の Stream で、Google guava のプリミティブを使って

List<Byte> listb = Pattern.compile("[\\w]{1,2}").matcher(hexstring)
.results().map(r->(byte)Integer.parseInt(r.group(), 16))
.collect(Collectors.toList());

byte[] ary = Bytes.toArray(listb);

Google guava は、Google guice を使用するなら依存関係で参照できる。

また、元の文字列に復元するには、、

String s = IntStream.range(0, ary.length).mapToObj(i->String.format("%02x", ary[i])).collect(Collectors.joining(""));

(参考)
http://oboe2uran.hatenablog.com/entry/2017/07/22/000044

http://oboe2uran.hatenablog.com/entry/2017/10/07/174948

http://oboe2uran.hatenablog.com/entry/2018/08/03/094849

Wicket 8.3.0 がリリースされた

Wicket 8.3.0 がリリースされてる。
https://wicket.apache.org/
目新しいのは、今までなかった StatelessResourceLink
Component タグの変更~記述したHTMLよりも表示時に変更されるもののリスナーのふるまい。
[WICKET-6626] が追加されてる。

Component #onComponentTag のリスナは、WebApplication に登録する。
だから、Page クラスで、、

getApplication().getOnComponentTagListeners().add(new IOnComponentTagListener(){
    @Override
    public void onComponentTag(Component component, ComponentTag tag){
          // TODO
    }
});

とするとこのページを訪れたセッションでは、全てこの IOnComponentTagListener
onComponentTag が各ページ(他のページ)でも動く。
レイアウトしたタグのIDやタグの属性の状況などリスナで把握できることになるが、
今更の感があり。
まだ効果的な使い方をなかなか思いつかない。

Markdownファイルを Python で PDF にする

Python スクリプト実行するディレクトリ配下の md ファイルをPDFにします。
途中 HTML にしてHTMLからPDFにします。
(条件)
HTML化した時のスタイルを任意のスタイルを
1つの スタイルシートファイルで指定するものとします。
以下、 スタイルシートファイルは、実行するディレクトリ配下に、
 wkstyle.less
というファイル名で存在するものとします。
(準備)

pip install Markdown
pip install pdfkit

Python ソース

# -*- coding: UTF-8 -*-
import markdown
import pdfkit
import base64
import re
import os
import glob

options = {
    'page-size': 'A4',
    'margin-top': '0.75in',
    'margin-right': '0.75in',
    'margin-bottom': '0.75in',
    'margin-left': '0.75in',
    'encoding': "UTF-8"
}

# Markdown 拡張、表と目次
mdextensions = [ "tables", "toc" ]

# ディレクトリ全探索
def find_all(directory):
    for root, dirs, files in os.walk(directory):
        yield root
        for file in files:
            yield os.path.join(root, file)

# カレントディレクトリから1番最初に見つかるファイル相対PATHを取得
def onefind_path(filename):
    for path in find_all('.'):
        if re.search(filename + '$', path):
            return path
    exit(1)

# 画像ファイル→ Base64 エンコード
def imageToB64encode(path):
    with open(path, 'rb') as f:
        return base64.b64encode(f.read()).decode()

# md ファイル → PDF
def convertPDF(mdpath, pdfpath, style):
    with open(mdpath, 'rt', encoding="utf-8") as f:
        text = f.read()
        # Markdown の import 文を除去
        text = re.sub('@import ".+"\n', '', text)
        # HTMLに変換
        body = markdown.Markdown(extensions=mdextensions).convert(text)
        # 画像は、base64 エンコードして <img src=data:image/png;base64,base64エンコード文字列"/> にする。
        for imgtag in re.findall('<img .* src=".+"', body):
            s = re.search('src=".+"', imgtag).group(0).replace('src="', '').replace('"', '')
            imgval = re.search('<img .* ', imgtag).group(0)
            imgval += 'src="data:image/' + s[-3:] + ';base64,' + imageToB64encode(s) + '"'
            body = body.replace(imgtag, imgval)
        html = '<html lang="ja"><meta charset="utf-8">'
        html += '<style>' + style + '</style>'
        html += '<body>' + body + '</body></html>'
        # PDFで出力
        pdfkit.from_string(html, pdfpath, options=options)

#########################
if __name__ == '__main__':
    # スタイルは、固定 wkstyle.less で 読込
    with open(onefind_path('wkstyle.less'), 'rt', encoding="utf-8") as f:
        wkstyle = f.read()
        for mdfile in glob.glob("*.md"):
            pdfname = re.sub('\.md$', '.pdf', mdfile)
            convertPDF(mdfile, pdfname, wkstyle)
            print("-----------------------------------")
            print("%s → %s" % (mdfile, pdfname))

Markdown で記述する表を HTMLでは、table にすることにより、
PDFに変換した時に表になるように、拡張オプションtable を指定します。
さらに、目次を付ける場合は、toc を付与します。

mdextensions = [ "tables", "toc" ]

markdown.Markdown(extensions=mdextensions)

md ファイルの方で目次が最終的に作られるように、

[TOC]

[TOC] の前には、必ず1行空けて目次を入れたい箇所に記述します。

wkstyle.less はサンプルとして以下のとおりです。
H2, H3, H4, H5 タグに見出し番号がつくようにしています。

body{
    font-size: 1rem;
    counter-reset: chapter1;
}
h2 {
  counter-reset: chapter2;
}
h3 {
  counter-reset: chapter3;
}
h4 {
  counter-reset: chapter4;
}
h5 {
}
h2::before {
    counter-increment: chapter1;
    content: counter(chapter1) ". ";
}
h3::before {
    counter-increment: chapter2;
    content: counter(chapter1) "." counter(chapter2) ". ";
}
h4::before {
    counter-increment: chapter3;
content: counter(chapter1) "." counter(chapter2) "." counter(chapter3) ". ";
}
h5::before {
    counter-increment: chapter4;
    content: counter(chapter1) "." counter(chapter2) "." counter(chapter3) "." counter(chapter4) ". ";
}
table{
   border-spacing: 0; border-collapse: collapse;
}
th, td{
   border: 1px solid #000000;
}

1つしかないファイル探索方法 by Python

カレントディレクトリから再帰で、1つしかないと想定されるファイル名の探索を
Python で。。

# -*- coding: utf-8 -*-
import os
import re
# ディレクトリ全探索
def find_all(directory):
    for root, dirs, files in os.walk(directory):
        yield root
        for file in files:
            yield os.path.join(root, file)

# カレントディレクトリから1番最初に見つかるファイル相対PATHを取得
def onefind_path(filename):
    for path in find_all('.'):
        if re.search(filename + '$', path):
            return path
    exit(1)

find_add() は使いまわせる
使用例、sample.css を探す

path = onefind_path('sample.css')
print(path)

wkhtmltopdf で任意に改ページを挿入する

wkhtmltopdf でPDFに変換する
https://wkhtmltopdf.org/

   <div style="page-break-after:always;"></div>

あるいは、罫線で

   <hr style="page-break-after:always;"/>

改ページしたい箇所に書き込んでおく。

md(Markdownファイル)から PDF を作成するのに、pandoc Prince とか使わずに、
一旦、HTMLに変換してPDFを作るつもりで、wkhtmltopdf を使おうと思った。
pandoc が動く環境を作るのが非常にめんどうだからだ。。。