我輩はブロガーではない。ネタもまだない

SASとかDelphiあたりの人様の役に立たないネタを提供します

SASで復元不可能な一意のダミー文字列

Twitter上で
SASで、患者IDから復元不可能な一意のダミー文字列を生成したい」
というつぶやきを見つけました。(超意訳)

ハッシュ関数で実現できそうですが、SHA256では長すぎるということでした。

確かに256bit、64バイトの文字列はIDには長すぎます。MD5でも32バイトです。


というわけで、どうにか一方向性と非可逆性を持つ文字列を短く出来ないか、ということを考えてみました。




ハッシュ関数を新しく考え出すのは当然ながらコストが掛かりすぎるので既存のハッシュ関数を加工することを考えます。

SASで扱える既存のハッシュ関数には、

  • MD5:128bit長、32バイト文字列
  • SHA256:256bit長、64バイト文字列

があり、それぞれ、以下のようなハッシュ値が得られます

f:id:japelin:20200807082053p:plain


16進数文字列はバイナリ文字列を4バイトずつに区切ってそれぞれを16進数で表現したものです。

  • バイナリ→10進数→16進数
  • 0001→1→1
  • 1001→9→9
  • 1100→12→C


したがってハッシュ値を5バイトずつに区切れば32進数で、6バイトずつに区切れば64進数で表示できます。
64進数はメールで使われるBASE64エンコーディングがあります。
つまりBASE64エンコーディングを実装すればMD5→22文字、SHA256→43文字で表現できることになります。
(BASE64のように「=」で埋める実装は必要ない)

さらに、7バイトで区切るとどうでしょう。128進数ということになりますが、残念ながら
ASCIコードの128文字には制御文字が含まれるため、IDのような文字列生成には向きません。


が、あるじゃないですか、我が日本には。1バイトの!半角カナが!
英数記号と半角カナを合わせると、大体128文字くらいになります。すごい!

早速実装してみます。
arrayステートメントで、BASEエンコーディングと同じようにA-Z、a-z、0-9、+、/、さらに
128進数にするための文字をその他の記号+半角カナをつかって定義します。
なお、除いた記号は&%\です。

%Macro MHash2Char(str,alg,bitlen);
  /* MD5は128bit、SHA256は256bit */
  %if %upcase(&alg)=MD5 %then %let hashbit=128;
  %else %if %upcase(&alg)=SHA256 %then %let hashbit=256;
  data _null_;
    /* 128進数に対応する文字列、64進数まではBASE64エンコーディングと同一 */
    array tr{128} $1 ('A','B','C','D','E','F','G','H','I','J'
                     ,'K','L','M','N','O','P','Q','R','S','T'
                     ,'U','V','W','X','Y','Z'
                     ,'a','b','c','d','e','f','g','h','i','j'
                     ,'k','l','m','n','o','p','q','r','s','t'
                     ,'u','v','w','x','y','z'
                     ,'0','1','2','3','4','5','6','7','8','9'
                     ,'+','/'
                     ,'!','#','$','(',')','*','-','.',':',';'
                     ,'<','=','>','?','@','[',']','^','_'
                     ,'ア','イ','ウ','エ','オ','カ','キ','ク','ケ','コ'
                     ,'サ','シ','ス','セ','ソ','タ','チ','ツ','テ','ト'
                     ,'ナ','ニ','ヌ','ネ','ノ','ハ','ヒ','フ','ヘ','ホ'
                     ,'マ','ミ','ム','メ','モ','ヤ','ユ','ヨ'
                     ,'ラ','リ','ル','レ','ロ','ワ','ン'
                     );
  
  
    length s1 $&hashbit
           s2 $&bitlen
           t1 $43
           ;
    /* 元の値からバイナリ文字列を取得 */
    s1=put(&alg(&str),$binary&hashbit..);
    t1='';
    do while(length(s1)>1);
      /* 指定したbit長だけ処理する */
      s2=subpad(s1,1,&bitlen);
      /* 桁が短ければ0で埋める */
      if length(s2)<&bitlen then s2=cats(s2,repeat('0',&bitlen));
      /* バイナリ文字列を10進数数値に変換して、配列の値を取得 */
      t1=cats(t1,tr{input(s2,binary&bitlen..)+1});
      /* 残ったバイナリ文字列は次で処理 */
      s1=substr(s1,&bitlen+1);
    end;
    call symputx('hash1',put(&alg(&str),$hex%eval(&hashbit./4).),'G');
    call symputx("hash2",t1,'G');
  run;
  
  %put;
  %put --%upcase(&alg)ハッシュ値;
  %put &hash1;
  %put;
  %put --%upcase(&alg)エンコーディング値;
  %put &hash2;
%Mend;


%Macro MChar2Hash(str);
  /* エンコード文字数+クオート2文字分で判別 */
  %if %length(&str)=21 %then %do;
    %let hashbit=128;
    %let alg=MD5;
    %let bitlen=7;
  %end; %else
  %if %length(&str)=24 %then %do;
    %let hashbit=128;
    %let alg=MD5;
    %let bitlen=6;
  %end; %else
  %if %length(&str)=39 %then %do;
    %let hashbit=256;
    %let alg=SHA256;
    %let bitlen=7;
  %end; %else
  %if %length(&str)=45 %then %do;
    %let hashbit=256;
    %let alg=SHA256;
    %let bitlen=6;
  %end;
  data _null_;
    array tr{128} $1 ('A','B','C','D','E','F','G','H','I','J'
                     ,'K','L','M','N','O','P','Q','R','S','T'
                     ,'U','V','W','X','Y','Z'
                     ,'a','b','c','d','e','f','g','h','i','j'
                     ,'k','l','m','n','o','p','q','r','s','t'
                     ,'u','v','w','x','y','z'
                     ,'0','1','2','3','4','5','6','7','8','9'
                     ,'+','/'
                     ,'!','#','$','(',')','*','-','.',':',';'
                     ,'<','=','>','?','@','[',']','^','_'
                     ,'ア','イ','ウ','エ','オ','カ','キ','ク','ケ','コ'
                     ,'サ','シ','ス','セ','ソ','タ','チ','ツ','テ','ト'
                     ,'ナ','ニ','ヌ','ネ','ノ','ハ','ヒ','フ','ヘ','ホ'
                     ,'マ','ミ','ム','メ','モ','ヤ','ユ','ヨ'
                     ,'ラ','リ','ル','レ','ロ','ワ','ン'
                     );
  
    length s1 $&hashbit.
           s2 $&bitlen
           t1 $1
           ;
    t1='';
    s1='';
    do i=1 to length(&str);
      t1=substr(&str,i,1);
      do j=1 to dim(tr);
        if t1=tr{j} then do;
          s2=put(j-1,binary&bitlen..);
          s1=cats(s1,s2);
        end;
      end;
    end;
    call symputx('hash1',put(input(s1,$binary&hashbit..),$hex%eval(&hashbit./4).),'G');
  run;
  %put;
  %put --%upcase(&alg)ハッシュ値の復元;
  %put &hash1;
%Mend;


実行してみましょう。

%put ;
%put MD5で22文字にエンコード
%MHash2Char('abc',md5,6);
%MChar2Hash("&hash2");

%put ;
%put MD5で19文字にエンコード
%MHash2Char('abc',md5,7);
%MChar2Hash("&hash2");

%put ;
%put SHA256で43文字にエンコード
%MHash2Char('abc',sha256,6);
%MChar2Hash("&hash2");

%put ;
%put SHA256で37文字にエンコード;
%MHash2Char('abc',sha256,7);
%MChar2Hash("&hash2");

結果は以下の通りです。
f:id:japelin:20200807082911p:plain

出来ました。

ちなみに、8バイトで区切ると、256進数となり、半角文字で表現できなくなってしまう、かつ
1文字=2バイトとなり7バイト区切りのときよりも長くなってしまうので意味がなさそうです。


参考:
MD5関数
SHA256関数