且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Java 和 C# 之间的加密差异

更新时间:2023-01-14 21:06:36

问题是 PasswordDeriveBytes 只定义了前 20 个字节 - 在这种情况下它是 PBKDF1(不是 2,正如您当前在 Java 代码中使用的那样).多次调用 getBytes 也可能会改变结果.一次或多次调用 getBytes 或超过 20 个字节的算法是 Microsoft 专有的,似乎在任何地方都没有描述.在 Mono 中,它甚至被描述为非修复,因为它可能不安全.

The problem is that PasswordDeriveBytes is only defined for the first 20 bytes - in that case it is PBKDF1 (not 2, as you are currently using in your Java code). Calling getBytes multiple times may also change the result. The algorithm for one or more calls to getBytes or for more than 20 bytes is Microsoft proprietary and doesn't seem to be described anywhere. In Mono it's even described as a non-fix as it may not be secure.

我强烈建议使用 RFC2898DeriveBytes确实实现了 PBKDF2.请注意仅将其用于 ASCII 输入,否则它可能与 Java 实现不兼容.

I would strongly suggest to use RFC2898DeriveBytes that does implement PBKDF2. Beware to only use it for ASCII input or it may not be compatible with the Java implementation.

唯一的其他选择是找出 Microsoft PasswordDeriveBytes 到 PBKDF1 的专有扩展(它只定义了高达 20 字节的散列大小的输出).我已经重新实现了下面的 Mono 版本.

The only other option is to figure out the proprietary extension of Microsoft PasswordDeriveBytes to PBKDF1 (which only defines output up to the hash size of 20 bytes). I've reimplemented the version of Mono below.

反复请求微软更新这个函数的 API 描述没有产生任何结果.如果您的结果不同,您可能需要阅读此错误报告.

Repeated request to Microsoft to update the API description of this function did not produce any results. You may want to read this bug report if your results differ.

这是专有的 Microsoft 扩展.基本上它首先计算 PBKDF-1 直到但不包括最后一次哈希迭代,称之为 HX.对于前 20 个字节,它只是执行另一个散列,因此它符合 PBKDF1.下一个哈希值是从 1 开始的计数器的 ASCII 表示(因此首先转换为 "1",然后转换为 0x31),然后是 HX 的字节.

This is the proprietary Microsoft extension. Basically it first calculates PBKDF-1 up to but not including the last hash iteration, call this HX. For the first 20 bytes it simply performs another hash, so it is compliant to PBKDF1. The next hashes are over the ASCII representation of a counter starting at 1 (so this is first converted to "1", then to 0x31), followed by the bytes of HX.

接下来是对 Mono 代码的极简、相当直接的转换:

What follows is a minimalistic, rather direct conversion from the Mono code:

public class PasswordDeriveBytes {

    private final MessageDigest hash;
    private final byte[] initial;
    private final int iterations;

    private byte[] output;
    private int hashnumber = 0;
    private int position = 0;

    public PasswordDeriveBytes(String password, byte[] salt) {
        try {
            this.hash = MessageDigest.getInstance("SHA-1");
            this.initial = new byte[hash.getDigestLength()];
            this.hash.update(password.getBytes(UTF_8));
            this.hash.update(salt);
            this.hash.digest(this.initial, 0, this.initial.length);
            this.iterations = 100;
        } catch (NoSuchAlgorithmException | DigestException e) {
            throw new IllegalStateException(e);
        }
    }

    public byte[] getBytes(int cb) {
        if (cb < 1)
            throw new IndexOutOfBoundsException("cb");
        byte[] result = new byte[cb];
        int cpos = 0;
        // the initial hash (in reset) + at least one iteration
        int iter = Math.max(1, iterations - 1);
        // start with the PKCS5 key
        if (output == null) {
            // calculate the PKCS5 key
            output = initial;
            // generate new key material
            for (int i = 0; i < iter - 1; i++)
                output = hash.digest(output);
        }
        while (cpos < cb) {
            byte[] output2 = null;
            if (hashnumber == 0) {
                // last iteration on output
                output2 = hash.digest(output);
            } else if (hashnumber < 1000) {
                String n = String.valueOf(hashnumber);
                output2 = new byte[output.length + n.length()];
                for (int j = 0; j < n.length(); j++)
                    output2[j] = (byte) (n.charAt(j));
                System.arraycopy(output, 0, output2, n.length(), output.length);
                // don't update output
                output2 = hash.digest(output2);
            } else {
                throw new SecurityException();
            }
            int rem = output2.length - position;
            int l = Math.min(cb - cpos, rem);
            System.arraycopy(output2, position, result, cpos, l);
            cpos += l;
            position += l;
            while (position >= output2.length) {
                position -= output2.length;
                hashnumber++;
            }
        }
        return result;
    }
}

或者,更加优化和可读,只留下输出缓冲区和位置在调用之间改变:

Or, a bit more optimized and readable, leaving just the output buffer and position to be changed in between calls:

public class PasswordDeriveBytes {

    private final MessageDigest hash;

    private final byte[] firstToLastDigest;
    private final byte[] outputBuffer;

    private int position = 0;

    public PasswordDeriveBytes(String password, byte[] salt) {
        try {
            this.hash = MessageDigest.getInstance("SHA-1");

            this.hash.update(password.getBytes(UTF_8));
            this.hash.update(salt);
            this.firstToLastDigest = this.hash.digest();

            final int iterations = 100;
            for (int i = 1; i < iterations - 1; i++) {
                hash.update(firstToLastDigest);
                hash.digest(firstToLastDigest, 0, firstToLastDigest.length);
            }

            this.outputBuffer = hash.digest(firstToLastDigest);

        } catch (NoSuchAlgorithmException | DigestException e) {
            throw new IllegalStateException("SHA-1 digest should always be available", e);
        }
    }

    public byte[] getBytes(int requested) {
        if (requested < 1) {
            throw new IllegalArgumentException(
                    "You should at least request 1 byte");
        }

        byte[] result = new byte[requested];

        int generated = 0;

        try {
            while (generated < requested) {
                final int outputOffset = position % outputBuffer.length;
                if (outputOffset == 0 && position != 0) {
                    final String counter = String.valueOf(position / outputBuffer.length);
                    hash.update(counter.getBytes(US_ASCII));
                    hash.update(firstToLastDigest);
                    hash.digest(outputBuffer, 0, outputBuffer.length);
                }

                final int left = outputBuffer.length - outputOffset;
                final int required = requested - generated;
                final int copy = Math.min(left, required);

                System.arraycopy(outputBuffer, outputOffset, result, generated, copy);

                generated += copy;
                position += copy;
            }
        } catch (final DigestException e) {
            throw new IllegalStateException(e);
        }
        return result;
    }
}

实际上并不是那么糟糕的安全性,因为字节由摘要彼此分开.所以关键拉伸还是比较OK的.请注意,那里有 Microsoft PasswordDeriveBytes 实现,其中包含错误和重复字节(请参阅上面的错误报告).此处不再赘述.

It's actually not all that bad security-wise as the bytes are separated from each other by the digest. So the key stretching is relatively OK. Note that there were Microsoft PasswordDeriveBytes implementations out there that contained a bug and repeated bytes (see bug report above). This is not reproduced here.

用法:

private static final String PASSWORD = "46dkaKLKKJLjdkdk;akdjafj";

private static final byte[] SALT = { 0x26, 0x19, (byte) 0x81, 0x4E,
        (byte) 0xA0, 0x6D, (byte) 0x95, 0x34 };

public static void main(String[] args) throws Exception {
    final Cipher desEDE = Cipher.getInstance("DESede/CBC/PKCS5Padding");

    final PasswordDeriveBytes myPass = new PasswordDeriveBytes(PASSWORD, SALT);
    final SecretKeyFactory kf = SecretKeyFactory.getInstance("DESede");
    final byte[] key = myPass.getBytes(192 / Byte.SIZE);
    final SecretKey desEDEKey = kf.generateSecret(new DESedeKeySpec(key));

    final byte[] iv = myPass.getBytes(desEDE.getBlockSize());

    desEDE.init(Cipher.ENCRYPT_MODE, desEDEKey, new IvParameterSpec(iv));

    final byte[] ct = desEDE.doFinal("owlstead".getBytes(US_ASCII));
}

Java 实现的旁注:


Side notes about the Java implementation:

  • 迭代次数太少,查看当前日期需要什么样的迭代次数
  • 密钥大小不正确,您应该创建 3 * 64 = 192 位而不是 196 位的密钥
  • 3DES 已过时,请改用 AES