加密 ODS 存档中的文件
Encrypt a file inside an ODS archive
我正在尝试重现 ODS(开放文档电子表格)存档中文件的 LibreOffice 加密。有关技术信息,请参阅 http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.html#__RefHeading__752811_826425813。
我找到的最佳摘要在 wikipedia:
When OpenDocument file is password protected the file structure of the bundle remains the same, but contents of XML files in the package are encrypted using following algorithm:
- The file contents are compressed with the DEFLATE algorithm.
- A checksum of a portion of the compressed file is computed (SHA-1 of the file contents, or SHA-1 of the first 1024 bytes of the file, or SHA-256 of the first 1024 bytes of the file) and stored so password correctness can be verified when decrypting.
- A digest (hash) of the user entered password in UTF-8 encoding is created and passed to the package component. ODF versions 1.0 and 1.1 only mandate support for the SHA-1 digest here, while version 1.2 recommends SHA-256.
- This digest is used to produce a derived key by undergoing key stretching with PBKDF2 using HMAC-SHA-1 with a salt of arbitrary length (in ODF 1.2 – it's 16 bytes in ODF 1.1 and below) generated by the random number generator for an arbitrary iteration count (1024 by default in ODF 1.2).
- The random number generator is used to generate a random initialization vector for each file.
- The initialization vector and derived key are used to encrypt the compressed file contents. ODF 1.0 and 1.1 use Blowfish in 8-bit cipher feedback mode, while ODF 1.2 considers it a legacy algorithm and allows Triple DES and AES (with 128, 196 or 256 bits), both in cipher block chaining mode, to be used instead.
我的未加密模块内容(编码:utf-8,换行符:LF)是:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic" script:moduleType="normal">REM ***** BASIC *****
REM Hello, world!
</script:module>
LibreOffice 生成并存储在 ODS 存档中的加密模块内容(十六进制):
[a3, f4, 61, 98, c1, c8, e8, b1, d3, fa, b0, bc, ef, 51, 87, da, 4c, d8, 92, c2, 09, 7f, 12, 19, 47, 44, af, 3b, 32, 9d, 4a, 33, eb, ab, c0, 45, 97, 00, 27, 60, cf, b3, 49, 55, 76, 46, e2, 3c, 35, a0, a7, a9, 8a, af, a3, cd, 3c, f3, 20, 5f, 83, 89, a4, 9c, d9, b5, a6, f5, db, 68, 0a, b4, d0, 15, 3e, 6d, af, c6, 16, 78, 29, 79, 42, cb, 56, e3, b1, cd, c1, a6, a0, 13, 91, 16, e3, 89, a8, c6, d4, 69, e8, ea, 87, e9, 9d, 09, bb, 03, a0, 6e, a0, 29, 37, 85, 9a, 59, fb, 47, 3a, 72, 1d, 85, 25, b0, 92, 37, 55, a4, eb, de, 03, eb, de, e1, b6, f3, f9, 7b, 3a, 09, 2c, ad, 8e, ff, 1e, a2, 79, 63, 12, 04, 93, 67, 3d, 59, 6c, e8, aa, ae, 37, 7e, 66, cf, 99, 54, 63, a5, ea, 31, 78, 44, b1, 54, be, 5a, af, 3f, 0d, bf, b5, ce, 98, c8, 7a, 44, 61, d4, 76, 69, 3b, 01, 6f, 27, ab, 5f, a2, b0, 98, 32, 52, 0c, 9c, 08, 0c, 6a, 0c, 54, e0, 83, dc, d0, ad, 3a, 0f, 0f, 75, 6f, e6, 0d, db, db, 50, a4, 2b, d3, 5f, 43, 7c, 2d, 16, fa, 87, 62, 09, f6, d2, 28, 31, b5, a0, be]
这是 LibreOffice 生成的清单的相关部分:
<manifest:file-entry manifest:full-path="Basic/Test/Module1.xml" manifest:media-type="text/xml" manifest:size="332">
<manifest:encryption-data manifest:checksum-type="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0#sha256-1k" manifest:checksum="/UdU2OKZn04r0e9O047PaWNqi7LGaHYN9mURmvMCM60=">
<manifest:algorithm manifest:algorithm-name="http://www.w3.org/2001/04/xmlenc#aes256-cbc" manifest:initialisation-vector="ZEk8JHG3bHu8kZw0VGOT+g=="/>
<manifest:key-derivation manifest:key-derivation-name="PBKDF2" manifest:key-size="32" manifest:iteration-count="100000" manifest:salt="jGIagiBnlFdvQctdCkYfRQ=="/>
<manifest:start-key-generation manifest:start-key-generation-name="http://www.w3.org/2000/09/xmldsig#sha256" manifest:key-size="32"/>
</manifest:encryption-data>
</manifest:file-entry>
密码是123
。
这是我的代码:
// imports
// needs a dependency on `org.bouncycastle/bcprov-jdk15on/1.65`
public class EncryptMacro {
public static void main(String[] args)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException,
IllegalBlockSizeException, InvalidKeyException, BadPaddingException,
InvalidAlgorithmParameterException, NoSuchPaddingException {
new EncryptMacro().encryptAsLO();
}
public void encryptAsLO() throws IOException, NoSuchAlgorithmException,
InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
// needs a dependency on `org.bouncycastle/bcprov-jdk15on/1.65`
Security.addProvider(new BouncyCastleProvider());
// copy the manifest parameters
int plainSize = 332;
byte[] checksum = Base64.decode("/UdU2OKZn04r0e9O047PaWNqi7LGaHYN9mURmvMCM60=");
byte[] iv = Base64.decode("ZEk8JHG3bHu8kZw0VGOT+g==");
int iterationCount = 100000;
byte[] salt = Base64.decode("jGIagiBnlFdvQctdCkYfRQ==");
int startKeySize = 32;
int keySize = 32;
// password
String password = "123"; // that's for testing purpose!
String plainText =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE script:module PUBLIC \"-//OpenOffice.org//DTD OfficeDocument 1.0//EN\" \"module.dtd\">\n<script:module xmlns:script=\"http://openoffice.org/2000/script\" script:name=\"Module1\" script:language=\"StarBasic\" script:moduleType=\"normal\">REM ***** BASIC *****\nREM Hello, world!\n</script:module>";
byte[] encrypted =
new byte[]{-93, -12, 97, -104, -63, -56, -24, -79, -45, -6, -80, -68, -17, 81, -121,
-38, 76, -40, -110, -62, 9, 127, 18, 25, 71, 68, -81, 59, 50, -99, 74, 51,
-21, -85, -64, 69, -105, 0, 39, 96, -49, -77, 73, 85, 118, 70, -30, 60, 53,
-96, -89, -87, -118, -81, -93, -51, 60, -13, 32, 95, -125, -119, -92, -100,
-39, -75, -90, -11, -37, 104, 10, -76, -48, 21, 62, 109, -81, -58, 22, 120,
41, 121, 66, -53, 86, -29, -79, -51, -63, -90, -96, 19, -111, 22, -29, -119,
-88, -58, -44, 105, -24, -22, -121, -23, -99, 9, -69, 3, -96, 110, -96, 41,
55, -123, -102, 89, -5, 71, 58, 114, 29, -123, 37, -80, -110, 55, 85, -92,
-21, -34, 3, -21, -34, -31, -74, -13, -7, 123, 58, 9, 44, -83, -114, -1, 30,
-94, 121, 99, 18, 4, -109, 103, 61, 89, 108, -24, -86, -82, 55, 126, 102,
-49, -103, 84, 99, -91, -22, 49, 120, 68, -79, 84, -66, 90, -81, 63, 13,
-65, -75, -50, -104, -56, 122, 68, 97, -44, 118, 105, 59, 1, 111, 39, -85,
95, -94, -80, -104, 50, 82, 12, -100, 8, 12, 106, 12, 84, -32, -125, -36,
-48, -83, 58, 15, 15, 117, 111, -26, 13, -37, -37, 80, -92, 43, -45, 95, 67,
124, 45, 22, -6, -121, 98, 9, -10, -46, 40, 49, -75, -96, -66};
// check the plain text size
byte[] source = plainText.getBytes(StandardCharsets.UTF_8);
this.check("Plain size", plainSize == source.length);
// deflate the content (see 1. above)
byte[] deflated = this.deflate(source);
// and check the checksum (see 2. above)
this.check("Deflated hash", Arrays.equals(checksum, this.getSha256_1k(deflated)));
// hash the password (see 3. above)
byte[] hashedPassword = this.getSha256_1k(password.getBytes(StandardCharsets.UTF_8));
char[] chars = new char[hashedPassword.length];
for (int i = 0; i < hashedPassword.length; i++) {
chars[i] = (char) hashedPassword[i];
}
this.check("Start key size", chars.length == startKeySize);
// or:
// char[] chars = password.toCharArray();
// get the key (see 4. above)
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec keySpec = new PBEKeySpec(chars, salt, iterationCount, keySize * 8);
SecretKey s = factory.generateSecret(keySpec);
Key key = new SecretKeySpec(s.getEncoded(), "AES");
// encrypt the data (see 6. above)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
// or:
// Cipher cipher = Cipher.getInstance("AES/CBC/ISO10126Padding"); // W3C padding
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] result = cipher.doFinal(deflated);
this.check("Encrypted", Arrays.equals(encrypted, result));
}
private byte[] deflate(byte[] data) throws IOException {
InputStream is = new ByteArrayInputStream(data);
ByteArrayOutputStream os = new ByteArrayOutputStream();
final byte[] buffer = new byte[16]; // for testing purpose
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, true);
DeflaterOutputStream dos = new DeflaterOutputStream(os, deflater);
int count = is.read(buffer);
while (count != -1) {
dos.write(buffer, 0, count);
count = is.read(buffer);
}
dos.close();
return os.toByteArray();
}
private byte[] getSha256_1k(byte[] data) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(data, 0, Math.min(data.length, 1024));
return digest.digest();
}
private void check(String text, boolean test) {
if (test) {
System.out.println(text + " ok");
} else {
System.out.println(text + " NOT ok");
System.exit(1);
}
}
}
输出为:
Plain size ok
Deflated hash ok
Start key size ok
Encrypted NOT ok
当然,我希望生成的加密数据与 ODS 存档中存在的数据相同。我尝试更改填充、密钥派生函数、将 password
直接传递给 PBEKeySpec
、三重检查密码等,但均未成功。我还查看了 LibreOffice (https://github.com/LibreOffice/core/tree/master/oox/source/crypto) 的源代码,但没能找到我的代码中的错误。 (如果这很重要,我在 Ubuntu 18.04.10 和 Java 8 上使用了 LibreOffice Calc 版本:6.0.7.3。)
我的问题是:我的错误在哪里,我该如何改正?
您的代码中存在三个问题:
- 根据规范,PBKDF2 与 HMAC-SHA1(而不是 HMAC-SHA256)一起使用,s。 3.4.2 Encryption Process
用PBKDF2WithHmacSHA256
导出的密钥s
是PBKDF2KeyImpl
的一个实例,它需要一个UTF8字符串作为密码(参见[=26=的文档] class)。不过这里的密码是hash,一般不兼容UTF8。一种可能的解决方案是将 PBEKeySpec
替换为 BouncyCastle 的 PKCS5S2ParametersGenerator
,它期望密码为字节数组(在 init
中)。对于此解决方案,请替换
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec keySpec = new PBEKeySpec(chars, salt, iterationCount, keySize * 8);
SecretKey s = factory.generateSecret(keySpec);
Key key = new SecretKeySpec(s.getEncoded(), "AES");
和
PBEParametersGenerator generator = new PKCS5S2ParametersGenerator(new SHA1Digest());
generator.init(hashedPassword, salt, iterationCount);
KeyParameter keyParam = (KeyParameter)generator.generateDerivedParameters(keySize * 8);
Key key = new SecretKeySpec(keyParam.getKey(), "AES");
使用的填充是ISO10126Padding,所以AES/CBC/PKCS7Padding
必须被AES/CBC/ISO10126Padding
代替。验证这一点的最简单方法是在不删除填充 (AES/CBC/NoPadding
) 的情况下解密目标密文 (encrypted
)。最后一个块是06230276DDC67229EB31E830A1D7500F
,符合ISO10126Padding。对于 ISO10126Padding,最后一个字节指定填充字节数,其中(除最后一个字节外)由 random 值组成。所以在这种情况下,最后 15 个字节是填充字节。
ISO10126Padding也是字节级别密文与
对比的原因
this.check("Encrypted", Arrays.equals(encrypted, result));
失败。因此,在比较密文时,不得考虑填充块。
我正在尝试重现 ODS(开放文档电子表格)存档中文件的 LibreOffice 加密。有关技术信息,请参阅 http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.html#__RefHeading__752811_826425813。
我找到的最佳摘要在 wikipedia:
When OpenDocument file is password protected the file structure of the bundle remains the same, but contents of XML files in the package are encrypted using following algorithm:
- The file contents are compressed with the DEFLATE algorithm.
- A checksum of a portion of the compressed file is computed (SHA-1 of the file contents, or SHA-1 of the first 1024 bytes of the file, or SHA-256 of the first 1024 bytes of the file) and stored so password correctness can be verified when decrypting.
- A digest (hash) of the user entered password in UTF-8 encoding is created and passed to the package component. ODF versions 1.0 and 1.1 only mandate support for the SHA-1 digest here, while version 1.2 recommends SHA-256.
- This digest is used to produce a derived key by undergoing key stretching with PBKDF2 using HMAC-SHA-1 with a salt of arbitrary length (in ODF 1.2 – it's 16 bytes in ODF 1.1 and below) generated by the random number generator for an arbitrary iteration count (1024 by default in ODF 1.2).
- The random number generator is used to generate a random initialization vector for each file.
- The initialization vector and derived key are used to encrypt the compressed file contents. ODF 1.0 and 1.1 use Blowfish in 8-bit cipher feedback mode, while ODF 1.2 considers it a legacy algorithm and allows Triple DES and AES (with 128, 196 or 256 bits), both in cipher block chaining mode, to be used instead.
我的未加密模块内容(编码:utf-8,换行符:LF)是:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic" script:moduleType="normal">REM ***** BASIC *****
REM Hello, world!
</script:module>
LibreOffice 生成并存储在 ODS 存档中的加密模块内容(十六进制):
[a3, f4, 61, 98, c1, c8, e8, b1, d3, fa, b0, bc, ef, 51, 87, da, 4c, d8, 92, c2, 09, 7f, 12, 19, 47, 44, af, 3b, 32, 9d, 4a, 33, eb, ab, c0, 45, 97, 00, 27, 60, cf, b3, 49, 55, 76, 46, e2, 3c, 35, a0, a7, a9, 8a, af, a3, cd, 3c, f3, 20, 5f, 83, 89, a4, 9c, d9, b5, a6, f5, db, 68, 0a, b4, d0, 15, 3e, 6d, af, c6, 16, 78, 29, 79, 42, cb, 56, e3, b1, cd, c1, a6, a0, 13, 91, 16, e3, 89, a8, c6, d4, 69, e8, ea, 87, e9, 9d, 09, bb, 03, a0, 6e, a0, 29, 37, 85, 9a, 59, fb, 47, 3a, 72, 1d, 85, 25, b0, 92, 37, 55, a4, eb, de, 03, eb, de, e1, b6, f3, f9, 7b, 3a, 09, 2c, ad, 8e, ff, 1e, a2, 79, 63, 12, 04, 93, 67, 3d, 59, 6c, e8, aa, ae, 37, 7e, 66, cf, 99, 54, 63, a5, ea, 31, 78, 44, b1, 54, be, 5a, af, 3f, 0d, bf, b5, ce, 98, c8, 7a, 44, 61, d4, 76, 69, 3b, 01, 6f, 27, ab, 5f, a2, b0, 98, 32, 52, 0c, 9c, 08, 0c, 6a, 0c, 54, e0, 83, dc, d0, ad, 3a, 0f, 0f, 75, 6f, e6, 0d, db, db, 50, a4, 2b, d3, 5f, 43, 7c, 2d, 16, fa, 87, 62, 09, f6, d2, 28, 31, b5, a0, be]
这是 LibreOffice 生成的清单的相关部分:
<manifest:file-entry manifest:full-path="Basic/Test/Module1.xml" manifest:media-type="text/xml" manifest:size="332">
<manifest:encryption-data manifest:checksum-type="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0#sha256-1k" manifest:checksum="/UdU2OKZn04r0e9O047PaWNqi7LGaHYN9mURmvMCM60=">
<manifest:algorithm manifest:algorithm-name="http://www.w3.org/2001/04/xmlenc#aes256-cbc" manifest:initialisation-vector="ZEk8JHG3bHu8kZw0VGOT+g=="/>
<manifest:key-derivation manifest:key-derivation-name="PBKDF2" manifest:key-size="32" manifest:iteration-count="100000" manifest:salt="jGIagiBnlFdvQctdCkYfRQ=="/>
<manifest:start-key-generation manifest:start-key-generation-name="http://www.w3.org/2000/09/xmldsig#sha256" manifest:key-size="32"/>
</manifest:encryption-data>
</manifest:file-entry>
密码是123
。
这是我的代码:
// imports
// needs a dependency on `org.bouncycastle/bcprov-jdk15on/1.65`
public class EncryptMacro {
public static void main(String[] args)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException,
IllegalBlockSizeException, InvalidKeyException, BadPaddingException,
InvalidAlgorithmParameterException, NoSuchPaddingException {
new EncryptMacro().encryptAsLO();
}
public void encryptAsLO() throws IOException, NoSuchAlgorithmException,
InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
// needs a dependency on `org.bouncycastle/bcprov-jdk15on/1.65`
Security.addProvider(new BouncyCastleProvider());
// copy the manifest parameters
int plainSize = 332;
byte[] checksum = Base64.decode("/UdU2OKZn04r0e9O047PaWNqi7LGaHYN9mURmvMCM60=");
byte[] iv = Base64.decode("ZEk8JHG3bHu8kZw0VGOT+g==");
int iterationCount = 100000;
byte[] salt = Base64.decode("jGIagiBnlFdvQctdCkYfRQ==");
int startKeySize = 32;
int keySize = 32;
// password
String password = "123"; // that's for testing purpose!
String plainText =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE script:module PUBLIC \"-//OpenOffice.org//DTD OfficeDocument 1.0//EN\" \"module.dtd\">\n<script:module xmlns:script=\"http://openoffice.org/2000/script\" script:name=\"Module1\" script:language=\"StarBasic\" script:moduleType=\"normal\">REM ***** BASIC *****\nREM Hello, world!\n</script:module>";
byte[] encrypted =
new byte[]{-93, -12, 97, -104, -63, -56, -24, -79, -45, -6, -80, -68, -17, 81, -121,
-38, 76, -40, -110, -62, 9, 127, 18, 25, 71, 68, -81, 59, 50, -99, 74, 51,
-21, -85, -64, 69, -105, 0, 39, 96, -49, -77, 73, 85, 118, 70, -30, 60, 53,
-96, -89, -87, -118, -81, -93, -51, 60, -13, 32, 95, -125, -119, -92, -100,
-39, -75, -90, -11, -37, 104, 10, -76, -48, 21, 62, 109, -81, -58, 22, 120,
41, 121, 66, -53, 86, -29, -79, -51, -63, -90, -96, 19, -111, 22, -29, -119,
-88, -58, -44, 105, -24, -22, -121, -23, -99, 9, -69, 3, -96, 110, -96, 41,
55, -123, -102, 89, -5, 71, 58, 114, 29, -123, 37, -80, -110, 55, 85, -92,
-21, -34, 3, -21, -34, -31, -74, -13, -7, 123, 58, 9, 44, -83, -114, -1, 30,
-94, 121, 99, 18, 4, -109, 103, 61, 89, 108, -24, -86, -82, 55, 126, 102,
-49, -103, 84, 99, -91, -22, 49, 120, 68, -79, 84, -66, 90, -81, 63, 13,
-65, -75, -50, -104, -56, 122, 68, 97, -44, 118, 105, 59, 1, 111, 39, -85,
95, -94, -80, -104, 50, 82, 12, -100, 8, 12, 106, 12, 84, -32, -125, -36,
-48, -83, 58, 15, 15, 117, 111, -26, 13, -37, -37, 80, -92, 43, -45, 95, 67,
124, 45, 22, -6, -121, 98, 9, -10, -46, 40, 49, -75, -96, -66};
// check the plain text size
byte[] source = plainText.getBytes(StandardCharsets.UTF_8);
this.check("Plain size", plainSize == source.length);
// deflate the content (see 1. above)
byte[] deflated = this.deflate(source);
// and check the checksum (see 2. above)
this.check("Deflated hash", Arrays.equals(checksum, this.getSha256_1k(deflated)));
// hash the password (see 3. above)
byte[] hashedPassword = this.getSha256_1k(password.getBytes(StandardCharsets.UTF_8));
char[] chars = new char[hashedPassword.length];
for (int i = 0; i < hashedPassword.length; i++) {
chars[i] = (char) hashedPassword[i];
}
this.check("Start key size", chars.length == startKeySize);
// or:
// char[] chars = password.toCharArray();
// get the key (see 4. above)
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec keySpec = new PBEKeySpec(chars, salt, iterationCount, keySize * 8);
SecretKey s = factory.generateSecret(keySpec);
Key key = new SecretKeySpec(s.getEncoded(), "AES");
// encrypt the data (see 6. above)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
// or:
// Cipher cipher = Cipher.getInstance("AES/CBC/ISO10126Padding"); // W3C padding
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] result = cipher.doFinal(deflated);
this.check("Encrypted", Arrays.equals(encrypted, result));
}
private byte[] deflate(byte[] data) throws IOException {
InputStream is = new ByteArrayInputStream(data);
ByteArrayOutputStream os = new ByteArrayOutputStream();
final byte[] buffer = new byte[16]; // for testing purpose
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, true);
DeflaterOutputStream dos = new DeflaterOutputStream(os, deflater);
int count = is.read(buffer);
while (count != -1) {
dos.write(buffer, 0, count);
count = is.read(buffer);
}
dos.close();
return os.toByteArray();
}
private byte[] getSha256_1k(byte[] data) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(data, 0, Math.min(data.length, 1024));
return digest.digest();
}
private void check(String text, boolean test) {
if (test) {
System.out.println(text + " ok");
} else {
System.out.println(text + " NOT ok");
System.exit(1);
}
}
}
输出为:
Plain size ok
Deflated hash ok
Start key size ok
Encrypted NOT ok
当然,我希望生成的加密数据与 ODS 存档中存在的数据相同。我尝试更改填充、密钥派生函数、将 password
直接传递给 PBEKeySpec
、三重检查密码等,但均未成功。我还查看了 LibreOffice (https://github.com/LibreOffice/core/tree/master/oox/source/crypto) 的源代码,但没能找到我的代码中的错误。 (如果这很重要,我在 Ubuntu 18.04.10 和 Java 8 上使用了 LibreOffice Calc 版本:6.0.7.3。)
我的问题是:我的错误在哪里,我该如何改正?
您的代码中存在三个问题:
- 根据规范,PBKDF2 与 HMAC-SHA1(而不是 HMAC-SHA256)一起使用,s。 3.4.2 Encryption Process
用
PBKDF2WithHmacSHA256
导出的密钥s
是PBKDF2KeyImpl
的一个实例,它需要一个UTF8字符串作为密码(参见[=26=的文档] class)。不过这里的密码是hash,一般不兼容UTF8。一种可能的解决方案是将PBEKeySpec
替换为 BouncyCastle 的PKCS5S2ParametersGenerator
,它期望密码为字节数组(在init
中)。对于此解决方案,请替换SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec keySpec = new PBEKeySpec(chars, salt, iterationCount, keySize * 8); SecretKey s = factory.generateSecret(keySpec); Key key = new SecretKeySpec(s.getEncoded(), "AES");
和
PBEParametersGenerator generator = new PKCS5S2ParametersGenerator(new SHA1Digest()); generator.init(hashedPassword, salt, iterationCount); KeyParameter keyParam = (KeyParameter)generator.generateDerivedParameters(keySize * 8); Key key = new SecretKeySpec(keyParam.getKey(), "AES");
使用的填充是ISO10126Padding,所以
AES/CBC/PKCS7Padding
必须被AES/CBC/ISO10126Padding
代替。验证这一点的最简单方法是在不删除填充 (AES/CBC/NoPadding
) 的情况下解密目标密文 (encrypted
)。最后一个块是06230276DDC67229EB31E830A1D7500F
,符合ISO10126Padding。对于 ISO10126Padding,最后一个字节指定填充字节数,其中(除最后一个字节外)由 random 值组成。所以在这种情况下,最后 15 个字节是填充字节。ISO10126Padding也是字节级别密文与
对比的原因this.check("Encrypted", Arrays.equals(encrypted, result));
失败。因此,在比较密文时,不得考虑填充块。