创建基于密码的加密密钥

本文讨论了创建基于密码的加密PBE密钥。

首先提醒您以前的要点–通常,在实际操作中,应将PBE密钥用作主密钥,该主密钥仅用于解锁工作密钥。 这具有三个主要优点:

  • 您可以有多个密码,例如,托管的恢复密钥,
  • 您无需更改密码即可更改密码,
  • 您可以更改工作密钥,而不必强行更改密码。

我将在以后的文章中讨论在数据库加密中使用工作密钥。

使用PBKDF2WithHmacSHA1的基于密码的加密密钥生成

Java过去没有创建PBE密钥的标准方法。 各个密码提供者提供了自己的生成器,但是识别和使用与您的密码匹配的生成器是一个痛苦的过程。

Java 7对此进行了更改。现在,所有JCE实现中都提供了一种标准的密钥生成算法。 它绝对可以用于生成AES密钥。 我已经看到了一个示例,该示例用于生成任意长度的密钥,但是我无法复制该行为–这可能是非标准扩展。

该算法采用四个参数。 第一个是密钥长度– AES密钥使用128。 其他可能的值是192和256位。 第二个是迭代次数。 您的wifi路由器使用4096次迭代,但是现在很多人建议至少进行10,000次迭代。

第三个参数是“盐”。 wifi路由器使用SSID,许多站点使用一个小文件,下面我将讨论另一种方法。 盐应足够大,以使熵大于密钥长度。 例如,如果要使用128位密钥,则应该(至少)具有128位随机二进制数据或大约22个随机字母数字字符。

最后一个参数是密码。 同样,熵应大于密钥长度。 在Webapp中,密码通常是由应用服务器通过JNDI提供的。

最后,我们既需要密码密钥,又需要IV,而不仅仅是密码密钥。 缺乏IV或使用较弱的IV是不熟悉密码学的人最常见的错误之一。 (请参阅: 不使用具有密码块链接模式的随机初始化矢量 [owasp.org]。)一种常见的方法是生成随机盐,并将其添加到密文中以供解密期间使用,但是我将讨论另一种使用密码和盐。

现在的代码。 首先,我们了解如何从密码和盐创建密码密钥和IV。 (我们待会儿讨论盐。)

public class PbkTest {
    private static final Provider bc = new BouncyCastleProvider();
    private static final ResourceBundle BUNDLE = ResourceBundle
            .getBundle(PbkTest.class.getName());
    private SecretKey cipherKey;
    private AlgorithmParameterSpec ivSpec;

    /**
     * Create secret key and IV from password.
     * 
     * Implementation note: I've believe I've seen other code that can extract
     * the random bits for the IV directly from the PBEKeySpec but I haven't
     * been able to duplicate it. It might have been a BouncyCastle extension.
     * 
     * @throws Exception
     */
    public void createKeyAndIv(char[] password) throws SecurityException,
            NoSuchAlgorithmException, InvalidKeySpecException {
        final String algorithm = "PBKDF2WithHmacSHA1";
        final SecretKeyFactory factory = SecretKeyFactory
                .getInstance(algorithm);
        final int derivedKeyLength = 128;
        final int iterations = 10000;

        // create salt
        final byte[][] salt = feistelSha1Hash(createSalt(), 1000);

        // create cipher key
        final PBEKeySpec cipherSpec = new PBEKeySpec(password, salt[0],
                iterations, derivedKeyLength);
        cipherKey = factory.generateSecret(cipherSpec);
        cipherSpec.clearPassword();

        // create IV. This is just one of many approaches. You do
        // not want to use the same salt used in creating the PBEKey.
        try {
            final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", bc);
            cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(
                    salt[1], 0, 16));
            ivSpec = new IvParameterSpec(cipher.doFinal(salt[1], 4, 16));
        } catch (NoSuchPaddingException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (InvalidAlgorithmParameterException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (InvalidKeyException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (BadPaddingException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (IllegalBlockSizeException e) {
            throw new SecurityException("unable to create IV", e);
        }
    }
}

我们可以简单地加载包含随机二进制数据的文件,但是使用Feistel密码可以使我们混合来自两个来源的熵。

/**
     * Create salt. Two values are provided to support creation of both a cipher
     * key and IV from a single password.
     * 
     * The 'left' salt is pulled from a file outside of the app context. this
     * makes it much harder for a compromised app to obtain or modify this
     * value. You could read it as classloader resource but that's not really
     * different from the properties file used below. Another possibility is to
     * load it from a read-only value in a database, ideally one with a
     * different schema than the rest of the application. (It could even be an
     * in-memory database such as H2 that contains nothing but keying material,
     * again initialized from a file outside of the app context.)
     * 
     * The 'right' salt is pulled from a properties file. It is possible to use
     * a base64-encoded value but administration is a lot easier if we just take
     * an arbitrary string and hash it ourselves. At a minimum it should be a
     * random mix-cased string of at least (120/5 = 24) characters.
     * 
     * The generated salts are equally strong.
     * 
     * Implementation note: since this is for demonstration purposes a static
     * string in used in place of reading an external file.
     */
    public byte[][] createSalt() throws NoSuchAlgorithmException {
        final MessageDigest digest = MessageDigest.getInstance("SHA1");
        final byte[] left = new byte[20]; // fall back to all zeroes
        final byte[] right = new byte[20]; // fall back to all zeroes

        // load value from file or database.
        // note: we use fixed value for demonstration purposes.
        final String leftValue = "this string should be read from file or database";
        if (leftValue != null) {
            System.arraycopy(digest.digest(leftValue.getBytes()), 0, left, 0,
                    left.length);
            digest.reset();
        }

        // load value from resource bundle.
        final String rightValue = BUNDLE.getString("salt");
        if (rightValue != null) {
            System.arraycopy(digest.digest(rightValue.getBytes()), 0, right, 0,
                    right.length);
            digest.reset();
        }

        final byte[][] salt = feistelSha1Hash(new byte[][] { left, right },
                1000);

        return salt;
    }

使用资源束(在类路径中可见)和从文件系统或数据库加载的字符串的实际实现是:

/**
     * Create salt. Two values are provided to support creation of both a cipher
     * key and IV from a single password.
     * 
     * The 'left' salt is pulled from a file outside of the app context. this
     * makes it much harder for a compromised app to obtain or modify this
     * value. You could read it as classloader resource but that's not really
     * different from the properties file used below. Another possibility is to
     * load it from a read-only value in a database, ideally one with a
     * different schema than the rest of the application. (It could even be an
     * in-memory database such as H2 that contains nothing but keying material,
     * again initialized from a file outside of the app context.)
     * 
     * The 'right' salt is pulled from a properties file. It is possible to use
     * a base64-encoded value but administration is a lot easier if we just take
     * an arbitrary string and hash it ourselves. At a minimum it should be a
     * random mix-cased string of at least (120/5 = 24) characters.
     * 
     * The generated salts are equally strong.
     * 
     * Implementation note: since this is for demonstration purposes a static
     * string in used in place of reading an external file.
     */
    public byte[][] createSalt() throws NoSuchAlgorithmException {
        final MessageDigest digest = MessageDigest.getInstance("SHA1");
        final byte[] left = new byte[20]; // fall back to all zeroes
        final byte[] right = new byte[20]; // fall back to all zeroes

        // load value from file or database.
        // note: we use fixed value for demonstration purposes.
        final String leftValue = "this string should be read from file or database";
        if (leftValue != null) {
            System.arraycopy(digest.digest(leftValue.getBytes()), 0, left, 0,
                    left.length);
            digest.reset();
        }

        // load value from resource bundle.
        final String rightValue = BUNDLE.getString("salt");
        if (rightValue != null) {
            System.arraycopy(digest.digest(rightValue.getBytes()), 0, right, 0,
                    right.length);
            digest.reset();
        }

        final byte[][] salt = feistelSha1Hash(new byte[][] { left, right },
                1000);

        return salt;
    }

最后,我们可以通过两种测试方法在实践中看到它:

/**
     * Obtain password. Architectually we'll want good "separation of concerns"
     * and we should get the cipher key and IV from a separate place than where
     * we use it.
     * 
     * This is a unit test so the password is stored in a properties file. In
     * practice we'll want to get it from JNDI from an appserver, or at least a
     * file outside of the appserver's directory.
     * 
     * @throws Exception
     */
    @Before
    public void setup() throws Exception {
        createKeyAndIv(BUNDLE.getString("password").toCharArray());
    }

    /**
     * Test encryption.
     * 
     * @throws Exception
     */
    @Test
    public void testEncryption() throws Exception {
        String plaintext = BUNDLE.getString("plaintext");

        Cipher cipher = Cipher.getInstance(BUNDLE.getString("algorithm"), bc);
        cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
        byte[] actual = cipher.doFinal(plaintext.getBytes());
        assertEquals(BUNDLE.getString("ciphertext"),
                new String(Base64.encode(actual), Charset.forName("UTF-8")));
    }

    /**
     * Test decryption.
     * 
     * @throws Exception
     */
    @Test
    public void testEncryptionAndDecryption() throws Exception {
        String ciphertext = BUNDLE.getString("ciphertext");

        Cipher cipher = Cipher.getInstance(BUNDLE.getString("algorithm"), bc);
        cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec);
        byte[] actual = cipher.doFinal(Base64.decode(ciphertext));

        assertEquals(BUNDLE.getString("plaintext"),
                new String(actual, Charset.forName("UTF-8")));
    }


翻译自: https://www.javacodegeeks.com/2013/10/creating-password-based-encryption-keys.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值