JAVA安全与加密

一. 随机数

随机算法的起源数字称为种子数(seed),在种子数的基础上进行一定的变换,从而产生需要的随机数字。

Random类中实现的随机是伪随机,也就是有规则的随机,因为它的种子是System.currentTimeMillis(),所以它的随机数都是可预测的。相同种子数的Random对象,相同次数生成的随机数字是完全相同的。所以在需要频繁生成随机数,或者安全要求较高的时候,不要使用Random。

SecureRandom类提供加密的强随机数生成器 (RNG)。当然,它的许多实现都是伪随机数生成器 (PRNG) 形式,这意味着它们将使用确定的算法根据实际的随机种子生成伪随机序列,也有其他实现可以生成实际的随机数,还有另一些实现则可能结合使用这两项技术。

SecureRandom和Random如果种子一样,产生的随机数也一样: 因为种子确定,随机数算法也确定,因此输出是确定的。只是说,SecureRandom类收集了一些随机事件,比如鼠标点击,键盘点击等等,SecureRandom 使用这些随机事件作为种子,因此种子是不可预测的,而不像Random默认使用系统当前时间的毫秒数作为种子,有规律可寻。

1. 创建SecureRandom

内置两种随机数算法,NativePRNG和SHA1PRNG。

(1) new

通过new来初始化,默认会使用NativePRNG算法生成随机数。
SecureRandom secureRandom = new SecureRandom()

(2) getInstance

SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG")//传算法名,如果不存在算法会抛出异常

2. SecureRandom的使用

SecureRandom继承自Random,所以也有nextInt之类的方法。

  • nextBytes(byte[] bytes) void //获取随机的一个byte数组,传入瓶子(产生的随机数放入了入参bytes)
  • generateSeed(int numBytes) byte[] //获取一个随机的byte数组,这个数组中的数通常可以用来做其他随机生成器的种子
  • SecureRandom.getSeed(int numBytes) byte[] //直接获得随机数,本质是包含了创建和使用两步
  • setSeed(byte[] seed) void //设置种子数
  • nextInt(int bound) int //生成[0,bound)之间的随机数

3. 实例

  /**
     * 生成一个随机数矩阵
     *
     * @param num        行数
     * @param rowLen     列数
     * @param seedLength 种子数长度
     * @author yaohuix
     * @time 2018/3/30 16:02
     */
    private List<List<Integer>> generateCakes(int num, int seedLength, int rowLen) {
        SecureRandom random = new SecureRandom();
        byte[] seeds = SecureRandom.getSeed(seedLength); //获取随机的byte数组,用来后续作为种子
        int counter = 0;
        int tmprows = 0;
        List<List<Integer>> CakesList = new ArrayList<List<Integer>>();
        while (num > tmprows) {
            List<Integer> list = new ArrayList<Integer>();
            while (counter < rowLen) {
                random.setSeed(seeds); //设置种子
                int cake = random.nextInt(38); //随机生成0-37的数字
                if (!list.contains(cake) && 0 != cake) {
                    list.add(cake);
                    counter++;
                }
                random.nextBytes(seeds); //随机获取新的byte数组用以作为下次的种子,不断循环
            }
            Collections.sort(list);
            tmprows++;
            counter = 0;
            CakesList.add(list);
        }
        return CakesList;
    }

4. 关于种子seed获取思路

产生高强度的随机数,有两个重要的因素:种子和算法。当然算法是可以有很多的,但是如何选择种子是非常关键的因素。那么如何得到一个近似随机的种子?这里有一个思路:收集计算机的各种信息,如键盘输入时间,CPU时钟,内存使用状态,硬盘空闲空间,IO延时,进程数量,线程数量等信息,来得到一个近似随机的种子。这样的话,除了理论上有破解的可能,实际上基本没有被破解的可能。而事实上,现在的高强度的随机数生成器都是这样实现的

二. 散列算法

散列是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。任何输入信息的变化,哪怕仅一位,都将导致散列结果的明显变化,这称之为雪崩效应。散列还应该是防冲突的,即找不出具有相同散列结果的两条信息。具有这些特性的散列结果就可以用于验证信息是否被修改

散列算法可以用来加密token生成签名, 以便token信息不暴露在网络同时还能验证登录的有效性。

1. MD5

全写: Message Digest Algorithm MD5(消息摘要算法第五版)
输出: 128bit

特点
a) 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
b) 容易计算:从原数据计算出MD5值很容易。
c) 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
d) 弱抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。
e) 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。

缺陷
Md5一度被认为十分靠谱。2009年,冯登国、谢涛二人利用差分攻击,将MD5的碰撞算法复杂度降低到221,极端情况下甚至可以降低至210。仅仅2^21的复杂度意味着即便是在2008年的计算机上,也只要几秒便可以找到一对碰撞。MD5已老, 在安全性要求较高的场合,不建议使用。

应用场景
一致性验证
数字签名
安全访问认证

(1) 算法实现
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(string.getBytes());

或者

InputStream in = new FileInputStream(file);
while ((len = in.read(buffer)) != -1) {
    md5.update(buffer, 0, len);
}
byte[] bytes = md5.digest();
(2) MD5加密安全性探讨

虽然说MD5加密本身是不可逆的,但并不是不可破译的,网上有关MD5解密的网站数不胜数,破解机制采用穷举法,就是我们平时说的跑字典。所以如何才能加大MD5破解的难度呢?

i) 对字符串多次MD5加密
public static String md5(String string, int times) {
    if (TextUtils.isEmpty(string)) {
        return "";
    }
    String md5 = md5(string);
    for (int i = 0; i < times - 1; i++) {
        md5 = md5(md5);
    }
    return md5(md5);
}
ii) MD5加盐

所谓加盐, 就是在原本需要加密的信息基础上,糅入其它内容salt:string+key(盐值key)然后进行MD5加密,加盐的方式也是多种多样。

  • 用string明文的hashcode作为盐,然后进行MD5加密
  • 随机生成一串字符串作为盐,然后进行MD5加密
public static String md5(String string, String slat) {
    if (TextUtils.isEmpty(string)) {
        return "";
    }
    MessageDigest md5 = null;
    try {
        md5 = MessageDigest.getInstance("MD5");
        byte[] bytes = md5.digest((string + slat).getBytes());
        String result = "";
        for (byte b : bytes) {
            String temp = Integer.toHexString(b & 0xff);
            if (temp.length() == 1) {
                temp = "0" + temp;
            }
            result += temp;
        }
        return result;
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return "";
}

2. SHA1

全名:安全哈希算法(Secure Hash Algorithm)
输出:160bit

(1) 与MD5比较

相同点
因为二者均由MD4导出,SHA-1和MD5彼此很相似。相应的,他们的强度和其他特性也是相似。

不同点
a) 对强行攻击的安全性:最显著和最重要的区别是SHA-1摘要比MD5摘要长32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对MD5是2128数量级的操作,而对SHA-1则是2160数量级的操作。这样,SHA-1对强行攻击有更大的强度。
b) 对密码分析的安全性:由于MD5的设计,易受密码分析的攻击,SHA-1显得不易受这样的攻击。
c) 速度:在相同的硬件上,SHA-1的运行速度比MD5慢。

三. 对称加密

1. AES加密

高级加密标准(Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。

接下来我们来实际看下具体怎么实现:

(1) 获取密钥生成器

KeyGenerator kg = KeyGenerator.getInstance("DESede")
分析:KeyGenerator类中提供了创建对称密钥的方法。getInstance(String algorithm)的参数指定加密算法的名称,可以是 “AES”、“DES”、“DESede”等。这些算法都可以实现加密。其中“DES”是目前最常用的对称加密算法,但安全性较差。“AES”是一种替代DES算法的新算法,可提供很好的安全性。

(2) 初始化密钥生成器

kg.init(168)init(int keysize, SecureRandom random)
分析:该步骤一般指定密钥的长度。如果该步骤省略的话,会根据算法自动使用默认的密钥长度。指定长度时,若第一步密钥生成器使用的是“DES”算法,则密钥长度必须是56位;若是“DESede”,则可以是112或168位,其中112位有效;若是“AES”,可以是128, 192或256位。

(3) 生成密钥

SecretKey k = kg.generateKey( )
分析:使用第一步获得的KeyGenerator类型的对象中generateKey()方法可以获得密钥。其类型为SecretKey类型,可用于以后的加密和解密。

i) 通过对象序列化方式将密钥保存在文件中
FileOutputStream  f = new FileOutputStream("key1.dat");
ObjectOutputStream b = new  ObjectOutputStream(f);
b.writeObject(k);

分析:ObjectOutputStream类中提供的writeObject方法可以将对象序列化,以流的方式进行处理。这里将文件输出流作为参数传递给ObjectOutputStream类的构造器,这样创建好的密钥将保存在文件key1.dat中。

代码与分析:

public static void main(String args[])
            throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance("DESede");
        kg.init(168);
        SecretKey k = kg.generateKey();
        FileOutputStream f = new FileOutputStream("key1.dat");
        ObjectOutputStream b = new ObjectOutputStream(f);
        b.writeObject(k);
    }
ii) 以字节保存对称密钥

★ 编程思路:
Java中所有的密钥类都有一个getEncoded( )方法,通过它可以从密钥对象中获取主要编码格式,其返回值是字节数组。其主要步骤为:

  • 获取主要编码格式
    byte[] kb=k.getEncoded()
    分析:执行SecretKey类型的对象k的getEncoded( )方法,返回的编码放在byte类型的数组中。
  • 保存密钥编码格式
FileOutputStream  f2=new FileOutputStream("keykb1.dat");
f2.write(kb);
(4) 转换为AES专用密钥

SecretKeySpec secretKeyspec = new SecretKeySpec(secretKey.getEncoded(), "AES")

(5) 创建密码器
//实例化
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
//使用密钥初始化,设置为加密模式/解密模式(Cipher.DECRYPT_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKeyspec);
(6) 加密/解密

byte[] doFinal(byte[] input)

实例:

    //Cipher密码  encrypt加密 decrypt解密 crypto秘密
    private final static String HEX = "0123456789ABCDEF";
    //AES是加密方式 CBC是工作模式 PKCS5Padding是填充模式
    private static final String CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
    private static final String AES = "AES";//AES 加密
    private static final String SHA1PRNG = "SHA1PRNG";//// SHA1PRNG 强随机种子算法, 要区别4.2以上版本的调用方法

    /*
     * 生成随机数,可以当做动态的密钥 加密和解密的密钥必须一致,不然将不能解密
     */
    public static String generateKey() {
        try {
            SecureRandom localSecureRandom = SecureRandom.getInstance(SHA1PRNG);
            byte[] bytes_key = new byte[20];
            localSecureRandom.nextBytes(bytes_key);
            String str_key = toHex(bytes_key);
            return str_key;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    // 对密钥进行处理
    private static byte[] getRawKey(byte[] seed) throws Exception {
        KeyGenerator kgen = KeyGenerator.getInstance(AES);
        //for android
        SecureRandom sr = null;
        // 在4.2以上版本中,SecureRandom获取方式发生了改变
         if (android.os.Build.VERSION.SDK_INT >= 17) {
             sr = SecureRandom.getInstance(SHA1PRNG, "Crypto");
         } else {
            sr = SecureRandom.getInstance(SHA1PRNG);
         }
        // for Java
        // secureRandom = SecureRandom.getInstance(SHA1PRNG);
        sr.setSeed(seed);
        kgen.init(128, sr); //256 bits or 128 bits,192bits
        //AES中128位密钥版本有10个加密循环,192比特密钥版本有12个加密循环,256比特密钥版本则有14个加密循环。
        SecretKey skey = kgen.generateKey();
        byte[] raw = skey.getEncoded();
        return raw;
    }

    /*
     * 加密
     */
    public static String encrypt(String key, String cleartext) {
        if (TextUtils.isEmpty(cleartext)) {
            return cleartext;
        }
        try {
            byte[] result = encrypt(key, cleartext.getBytes());
            return Base64.encode(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /*
    * 加密
    */
    private static byte[] encrypt(String key, byte[] clear) throws Exception {
        byte[] raw = getRawKey(key.getBytes());
        SecretKeySpec skeySpec = new SecretKeySpec(raw, AES);
        Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
        byte[] encrypted = cipher.doFinal(clear);
        return encrypted;
    }

    /*
    * 解密
    */
    public static String decrypt(String key, String encrypted) {
        if (TextUtils.isEmpty(encrypted)) {
            return encrypted;
        }
        try {
            byte[] enc = Base64.decodeToBytes(encrypted);
            byte[] result = decrypt(key, enc);
            return new String(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /*
     * 解密
     */
    private static byte[] decrypt(String key, byte[] encrypted) throws Exception {
        byte[] raw = getRawKey(key.getBytes());
        SecretKeySpec skeySpec = new SecretKeySpec(raw, AES);
        Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);
        byte[] decrypted = cipher.doFinal(encrypted);
        return decrypted;
    }


    //二进制转字符
    public static String toHex(byte[] buf) {
        if (buf == null)
            return "";
        StringBuffer result = new StringBuffer(2 * buf.length);
        for (int i = 0; i < buf.length; i++) {
            appendHex(result, buf[i]);
        }
        return result.toString();
    }


    private static void appendHex(StringBuffer sb, byte b) {
        sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f));
    }

推荐阅读更多精彩内容

  • 概述 之前一直对加密相关的算法知之甚少,只知道类似DES、RSA等加密算法能对数据传输进行加密,且各种加密算法各有...
    Henryzhu阅读 810评论 0 12
  • 文前说明作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种...
    羽杰阅读 5,041评论 1 7
  • 本文主要介绍移动端的加解密算法的分类、其优缺点特性及应用,帮助读者由浅入深地了解和选择加解密算法。文中会包含算法的...
    voyagelab阅读 5,521评论 5 29
  • 又是一年生日,第一次记得这么清晰。 23年了,原来已经过了22个生日了,只是,这也只是一个模糊冰冷的数字。 很久之...
    岚风的叶子阅读 19评论 0 0
  • 如果不是我妈问我吃了汤圆没,我不会太在意今天是元宵节。对于大多数上班的人来说,年,止于初七。不知道是现在的人太忙了...
    龙江石阅读 1,310评论 0 10