SASで復元不可能な一意のダミー文字列
Twitter上で
「SASで、患者IDから復元不可能な一意のダミー文字列を生成したい」
というつぶやきを見つけました。(超意訳)
SASで、任意の文字列 (患者ID) から一意のダミー文字列を生成し、かつダミー文字列から元の文字列は復元不可能とする方法が思い付きません
— Jane Smith (@Poswith1) 2020年8月4日
種類が異なるデータが複数あり、最終的に結合することを考えると最初に患者IDとダミーIDの対応表を作るのが定石ですが、対応表はデータ管理が必要で省きたいです
ハッシュ関数で実現できそうですが、SHA256では長すぎるということでした。
確かに256bit、64バイトの文字列はIDには長すぎます。MD5でも32バイトです。
というわけで、どうにか一方向性と非可逆性を持つ文字列を短く出来ないか、ということを考えてみました。
ハッシュ関数を新しく考え出すのは当然ながらコストが掛かりすぎるので既存のハッシュ関数を加工することを考えます。
- MD5:128bit長、32バイト文字列
- SHA256:256bit長、64バイト文字列
があり、それぞれ、以下のようなハッシュ値が得られます
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");
結果は以下の通りです。
出来ました。
ちなみに、8バイトで区切ると、256進数となり、半角文字で表現できなくなってしまう、かつ
1文字=2バイトとなり7バイト区切りのときよりも長くなってしまうので意味がなさそうです。