001/*
002 * Copyright (c) 2009 The openGion Project.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied. See the License for the specific language
014 * governing permissions and limitations under the License.
015 */
016package org.opengion.fukurou.security;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.InputStream;                                                                     // 5.10.9.0 (2019/03/01)
021import java.io.IOException;
022import java.nio.ByteBuffer;                                                                     // 5.5.2.6 (2012/05/25)
023import java.nio.channels.FileChannel;                                           // 5.7.2.1 (2014/01/17)
024import java.nio.charset.Charset;                                                        // 5.5.2.6 (2012/05/25)
025import org.opengion.fukurou.system.Closer;                                      // 5.5.2.6 (2012/05/25)
026import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
027import org.opengion.fukurou.model.FileOperation;                        // 5.10.9.0 (2019/03/01)
028
029import javax.crypto.spec.SecretKeySpec;
030import javax.crypto.Cipher;
031
032import java.security.MessageDigest;
033import java.security.NoSuchAlgorithmException;
034import java.security.GeneralSecurityException;                          // 5.7.2.1 (2014/01/17)
035import java.security.DigestInputStream;                                         // 5.10.9.0 (2019/03/01)
036
037/**
038 * HybsCryptography は、セキュリティ強化の為の Hybs独自の暗号化クラスです。
039 *
040 * このクラスは、暗号化キーを受け取り、それに基づいて暗号化/復号化を行います。
041 * ここでの暗号化は、秘密キー方式でバイト文字列に変換されたものを、16進アスキー文字に
042 * 直して、扱っています。よって、暗号化/復号化共に、文字列として扱うことが可能です。
043 *
044 * @og.rev 4.0.0.0 (2005/08/31) 新規追加
045 * @og.rev 5.9.10.0 (2019/03/01) クラウドストレージ対応を追加。(Fileクラスを拡張)
046 *
047 * @og.group ライセンス管理
048 *
049 * @version  4.0
050 * @author   Kazuhiko Hasegawa
051 * @since    JDK5.0,
052 */
053public final class HybsCryptography {
054        private final SecretKeySpec sksSpec ;
055        private static final String CIPHER_TYPE = "Blowfish" ;
056
057        /**
058         * 数字から16進文字に変換するテーブルです。
059         */
060        private static final char[] HEXA_DECIMAL = 
061                { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
062                  'a', 'b', 'c', 'd', 'e', 'f' };
063
064        /**
065         * プラットフォーム依存のデフォルトの Charset です。
066         * プラットフォーム依存性を考慮する場合、エンコード指定で作成しておく事をお勧めします。
067         *
068         * @og.rev 5.5.2.6 (2012/05/25) findbugs対応
069         */
070        private static final Charset DEFAULT_CHARSET = Charset.defaultCharset() ;
071
072        // 注意:秘密キーは、8の倍数でないといけない。
073        private static final String HYBS_CRYPT_KEY = "2a5a88891d37ae59" ;
074
075        /**
076         * 内部設定の秘密鍵を使用して,暗号化を行うオブジェクトを構築します。
077         * ここでの暗号化は、Java標準のセキュリティパッケージを使用しています。
078         * 
079         * @og.rev 6.2.5.0 (2015/06/05) 引数付コンストラクタを使用
080         */
081        public HybsCryptography() {
082                this( HYBS_CRYPT_KEY );
083        }
084
085        /**
086         * 秘密鍵の文字列を受け取って,暗号化を行うオブジェクトを構築します。
087         * ここでの暗号化は、Java標準のセキュリティパッケージを使用しています。
088         * 秘密鍵のサイズを、8 の倍数 (32 以上 448 以下) にする必要があります。
089         * 
090         * @og.rev 5.8.8.0 (2015/06/05) null時の挙動はデフォルトキーを利用する
091         *
092         * @param       cryptKey        暗号化を行う秘密鍵
093         */
094        public HybsCryptography( final String cryptKey ) {
095                // 5.8.8.0 (2015/06/05) null時はデフォルトキーを利用
096                final String useKey;
097                if( cryptKey == null || cryptKey.length() == 0 ){
098                        useKey = HYBS_CRYPT_KEY;
099                }
100                else{
101                        useKey = cryptKey;
102                }
103                sksSpec = new SecretKeySpec( useKey.getBytes( DEFAULT_CHARSET ), CIPHER_TYPE ); 
104        }
105
106        /**
107         * セキュリティカラムのDBTyepに対してHybs独自の暗号化を行います。
108         * 暗号化されたデータは、通常 byte 文字ですが、16進数アスキー文字列に変換
109         * したものを返します。
110         * この暗号化では、引数が null の場合は、ゼロ文字列を返します。
111         *
112         * @og.rev 5.7.2.1 (2014/01/17) Exceptionをまとめます。
113         *
114         * @param       org     暗号化を行う元の文字列
115         *
116         * @return      暗号化された文字列(HEXADECIMAL化)
117         * @og.rtnNotNull
118         */
119        public String encrypt( final String org ) {
120                if( org == null || org.isEmpty() ) { return ""; }
121
122                try {
123                        final Cipher cipher = Cipher.getInstance( CIPHER_TYPE );
124                        cipher.init( Cipher.ENCRYPT_MODE, sksSpec );
125                        final byte[] encrypted = cipher.doFinal( org.getBytes( DEFAULT_CHARSET ) );             // 5.5.2.6 (2012/05/25) findbugs対応
126
127                        return byte2hexa( encrypted );
128                }
129                // 5.7.2.1 (2014/01/17) Exceptionをまとめます。
130                catch( final GeneralSecurityException   ex ) {
131                        final String errMsg = "暗号化処理に失敗しました。[" + org + "]"
132                                                        + ex.getMessage() ;
133                        throw new OgRuntimeException( errMsg,ex );
134                }
135        }
136
137        /**
138         * セキュリティカラムのDBTyepに対してHybs独自の復号化を行います。
139         * ここでの復号化は、encrypt で暗号化された文字を戻す場合に使用します。
140         * この復号化では、null は復号化できないため、ゼロ文字列を返します。
141         *
142         * @og.rev 5.7.2.1 (2014/01/17) Exceptionをまとめます。
143         *
144         * @param       hex     復号化を行う暗号化された16進数アスキー文字列
145         *
146         * @return      復号化された元の文字列
147         * @og.rtnNotNull
148         */
149        public String decrypt( final String hex ) {
150                if( hex == null || hex.isEmpty() ) { return ""; }
151
152                try {
153                        final Cipher cipher = Cipher.getInstance( CIPHER_TYPE );
154                        cipher.init( Cipher.DECRYPT_MODE, sksSpec );
155                        final byte[] encrypted = hexa2byte( hex );
156                        final byte[] decrypted = cipher.doFinal( encrypted );
157                        return new String( decrypted,DEFAULT_CHARSET );         // 5.5.2.6 (2012/05/25) findbugs対応
158                }
159                // 5.7.2.1 (2014/01/17) Exceptionをまとめます。
160                catch( final GeneralSecurityException   ex ) {
161                        final String errMsg = "復号化処理に失敗しました。[" + hex + "]"
162                                                        + ex.getMessage() ;
163                        throw new OgRuntimeException( errMsg,ex );
164                }
165        }
166
167        /**
168         * バイト配列を16進数アスキー文字列に変換します。
169         *
170         * バイト配列を、2文字の0~9,a~fのアスキーに変換されます。
171         * これにより、すべての文字を、アスキー化できます。
172         * アスキー化で、上位が0F以下の場合でも、0 を出すことで、固定長に変換します。
173         *
174         * よって、入力バイトの2倍のlength()を持ったStringを作成します。
175         *
176         * @param       input バイト配列
177         *
178         * @return      16進数アスキー文字列
179         */
180        public static String byte2hexa( final byte[] input ) {
181                String rtn = null;
182                if( input != null && input.length > 0 ) {
183                        final int len = input.length ;
184                        char[] ch = new char[len*2];
185                        for( int i=0; i<len; i++ ) {
186                                final int high = (input[i] & 0xf0) >> 4 ;
187                                final int low  = input[i] & 0x0f ;
188                                ch[i*2]   = HEXA_DECIMAL[high];
189                                ch[i*2+1] = HEXA_DECIMAL[low];
190                        }
191                        rtn =  new String(ch);
192                }
193                return rtn;
194        }
195
196        /**
197         * 16進数アスキー文字列をバイト配列に変換します。
198         *
199         * 2文字の0~9,a~fのアスキー文字列を、バイト配列に変換されます。
200         *
201         * よって、入力Stringの1/2倍のlengthを持ったバイト配列を作成します。
202         *
203         * @param       input 16進数アスキー文字列
204         *
205         * @return      バイト配列
206         */
207        public static byte[] hexa2byte( final String input ) {
208                byte[] rtn = null;
209                if( input != null ) {
210                        final int len = input.length() ;
211                        rtn = new byte[len/2];
212                        for( int i=0; i<len/2; i++ ) {
213                                char ch = input.charAt( i*2 );
214                                final int high = ch < 'a' ? ch-'0' : ch-'a'+10 ;
215                                ch = input.charAt( i*2+1 );
216                                final int low  = ch < 'a' ? ch-'0' : ch-'a'+10 ;
217                                rtn[i] = (byte)(high << 4 | low);
218                        }
219                }
220                return rtn;
221        }
222
223        /**
224         * MessageDigestにより、MD5 でハッシュした文字に変換します。
225         *
226         * MD5で、16Byteのバイトに変換されますが、ここでは、16進数で文字列に変換しています。
227         *
228         * 変換方法は、各バイトの上位/下位を16進文字列に変換後、連結しています。
229         * これは、Tomcat等の digest 認証(MD5使用時)と同じ変換方式です。
230         * 連結後の文字列長は、32バイト(固定)になります。
231         *
232         * @og.rev 5.2.2.0 (2010/11/01) util.StringUtil から移動
233         *
234         * @param       input 変換前の文字列
235         *
236         * @return      MD5でハッシュした文字列。32バイト(固定)
237         */
238        public static String getMD5( final String input ) {
239                String rtn = null;
240                if( input != null ) {
241                        try {
242                                final MessageDigest md5 = MessageDigest.getInstance( "MD5" );
243                                md5.update( input.getBytes( DEFAULT_CHARSET ) );        // 5.5.2.6 (2012/05/25) findbugs対応
244                                final byte[] out = md5.digest();
245                                rtn = byte2hexa( out );
246                        }
247                        catch( final NoSuchAlgorithmException ex ) {
248                                final String errMsg = "MessageDigestで失敗しました。[" + input + "]"
249                                                        + ex.getMessage() ;
250                                throw new OgRuntimeException( errMsg,ex );
251                        }
252                }
253                return rtn;
254        }
255
256        /**
257         * MessageDigestにより、MD5 でハッシュした文字に変換します。
258         * 
259         * MD5で、16Byteのバイトに変換されますが、ここでは、16進数で文字列に変換しています。
260         *
261         * 変換方法は、各バイトの上位/下位を16進文字列に変換後、連結しています。
262         * これは、Tomcat等の digest 認証(MD5使用時)と同じ変換方式です。
263         * 連結後の文字列長は、32バイト(固定)になります。
264         * 下記サイトを参考に作成しています。
265         * https://stackoverflow.com/questions/304268/getting-a-files-md5-checksum-in-java
266         * 
267         * @og.rev 5.9.10.0 (2019/03/01) 新規追加。クラウドストレージ対応。
268         * 
269         * @param       input 変換前のFileOperationオブジェクト
270         *
271         * @return      MD5でハッシュした文字列。32バイト(固定)
272         */
273        public static String getMD5( final FileOperation input ) {
274                String rtn = null;
275                if( input != null ) {
276
277                        InputStream is = null;
278                        DigestInputStream dis = null;
279                        try {
280                                final MessageDigest md5 = MessageDigest.getInstance( "MD5" );
281                                is = input.read();
282                                dis = new DigestInputStream(is, md5);
283                                
284                                while(dis.read() > 0) {
285                                        // disを読み込んで、ダイジェスト情報を更新
286                                }
287                                
288                                // ダイジェスト情報を取得
289                                final byte[] out = md5.digest();
290                                rtn = byte2hexa( out );
291                        }
292                        catch( NoSuchAlgorithmException ex ) {
293                                final String errMsg = "MessageDigestで MD5 インスタンスの作成に失敗しました。[" + input + "]"
294                                                        + ex.getMessage() ;
295                                throw new RuntimeException( errMsg,ex );
296                        }
297                        catch( IOException ex ) {
298                                final String errMsg = "ファイルの読み取りを失敗しました。[" + input + "]"
299                                                        + ex.getMessage() ;
300                                throw new RuntimeException( errMsg,ex );
301                        }
302                        finally {
303                                Closer.ioClose(dis);
304                                Closer.ioClose(is);
305                        }
306                }
307                return rtn;
308        }
309
310        /**
311         * MessageDigestにより、MD5 でハッシュした文字に変換します。
312         *
313         * MD5で、16Byteのバイトに変換されますが、ここでは、16進数で文字列に変換しています。
314         *
315         * 変換方法は、各バイトの上位/下位を16進文字列に変換後、連結しています。
316         * これは、Tomcat等の digest 認証(MD5使用時)と同じ変換方式です。
317         * 連結後の文字列長は、32バイト(固定)になります。
318         *
319         * @og.rev 5.7.2.1 (2014/01/17) Exceptionをまとめます。
320         * @og.rev 5.9.10.0 (2019/03/01) クラウドストレージ対応を追加
321         *
322         * @param       input 変換前のFile
323         *
324         * @return      MD5でハッシュした文字列。32バイト(固定)
325         */
326        public static String getMD5( final File input ) {
327                // 2019/X FileOperationクラスの場合は、クラウドストレージ対応のメソッドを実行します。 oota tmp
328                if(input instanceof FileOperation) {
329                        return getMD5((FileOperation)input);
330                }
331
332                String rtn = null;
333                if( input != null ) {
334                        FileInputStream fis     = null;
335                        FileChannel             fc      = null;
336                        try {
337                                final MessageDigest md5 = MessageDigest.getInstance( "MD5" );
338                                fis = new FileInputStream( input );
339                                fc  =fis.getChannel();
340                                final ByteBuffer bb = fc.map( FileChannel.MapMode.READ_ONLY , 0L , fc.size() );
341                                md5.update( bb );
342                                final byte[] out = md5.digest();
343                                rtn = byte2hexa( out );
344                        }
345                        catch( final NoSuchAlgorithmException ex ) {
346                                final String errMsg = "MessageDigestで MD5 インスタンスの作成に失敗しました。[" + input + "]"
347                                                        + ex.getMessage() ;
348                                throw new OgRuntimeException( errMsg,ex );
349                        }
350                        catch( final IOException ex ) {
351                                final String errMsg = "ファイルの読み取りを失敗しました。[" + input + "]"
352                                                        + ex.getMessage() ;
353                                throw new OgRuntimeException( errMsg,ex );
354                        }
355                        finally {
356                                Closer.ioClose( fc );
357                                Closer.ioClose( fis );
358                        }
359                }
360                return rtn;
361        }
362
363        /**
364         * MessageDigestにより、SHA1 でハッシュした文字に変換します。
365         *
366         * 16進数で文字列に変換しています。
367         *
368         * 変換方法は、各バイトの上位/下位を16進文字列に変換後、連結しています。
369         * これは、Tomcat等の digest 認証と同じ変換方式です。
370         *
371         * @og.rev 5.9.27.1 (2010/12/08) 新規作成
372         *
373         * @param       input 変換前の文字列
374         *
375         * @return      SHA1でハッシュした文字列。32バイト(固定)
376         */
377        public static String getSHA1( final String input ) {
378                String rtn = null;
379                if( input != null ) {
380                        try {
381                                final MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
382                                sha1.update( input.getBytes( DEFAULT_CHARSET ) );       
383                                final byte[] out = sha1.digest();
384                                rtn = byte2hexa( out );
385                        }
386                        catch( final NoSuchAlgorithmException ex ) {
387                                final String errMsg = "MessageDigestで失敗しました。[" + input + "]"
388                                                                                + ex.getMessage() ;
389                                throw new RuntimeException( errMsg,ex );
390                        }
391                }
392                return rtn;
393        }
394
395        /**
396         * MessageDigestにより、SHA-512 でハッシュした文字に変換します。
397         *
398         * 16進数で文字列に変換しています。
399         *
400         * 変換方法は、各バイトの上位/下位を16進文字列に変換後、連結しています。
401         * これは、Tomcat等の digest 認証と同じ変換方式です。
402         *
403         * @og.rev 5.10.10.2 (2019/04/12) 新規作成
404         *
405         * @param       input 変換前の文字列
406         *
407         * @return      SHA-512でハッシュした文字列 128バイト
408         */
409        public static String getSHA512( final String input ) {
410                String rtn = null;
411                if( input != null ) {
412                        try {
413                                final MessageDigest sha1 = MessageDigest.getInstance("SHA-512");
414                                sha1.update( input.getBytes( DEFAULT_CHARSET ) );       
415                                final byte[] out = sha1.digest();
416                                rtn = byte2hexa( out );
417                        }
418                        catch( final NoSuchAlgorithmException ex ) {
419                                final String errMsg = "MessageDigestで失敗しました。[" + input + "]"
420                                                        + ex.getMessage() ;
421                                throw new RuntimeException( errMsg,ex );
422                        }
423                }
424                return rtn;
425        }
426
427        /**
428         * 暗号化のテストを行う為のメインメソッド
429         *
430         * java HybsCryptography KEY TEXT で起動します。
431         *   KEY  : 秘密鍵(8 の倍数 (32 以上 448 以下)文字)
432         *   TEXT : 変換する文字列
433         *
434         * @og.rev 5.2.2.0 (2010/11/01) 循環参照の解消(LogWriter 削除)
435         *
436         * @param       args    引数配列
437         */
438        public static void main( final String[] args ) {
439                if( args.length != 2 ) {
440                        System.out.println( "java HybsCryptography KEY TEXT" );
441                        System.out.println( "  KEY  : 秘密鍵(8 の倍数 (32 以上 448 以下)文字)" );
442                        System.out.println( "  TEXT : 変換する文字列" );
443                        return;
444                }
445
446                final HybsCryptography cript = new HybsCryptography( args[0] );
447
448                System.out.println( "IN   TEXT : " + args[1] );
449
450                final String hexa = cript.encrypt( args[1] );
451                System.out.println( "HEXA TEXT : " + hexa );
452
453                final String data = cript.decrypt( hexa );
454                System.out.println( "OUT  DATA : " + data );
455        }
456}