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