LibPDF: Implement 7.6.4.3.3 Algorithm 2.A: Retrieve file encryption key

...for handlers of revision 6.

The spec for this algorithm has several quirks:

1. It describes how to authenticate a password as an owner password,
   but it redundantly inlines the description of algorithm 12 instead
   of referring to it. We just call that algorithm here.

2. It does _not_ describe how to authenticate a password as a user
   password before using the password to compute the file encryption
   key using an intermediate user key, despite the latter step that
   computes the file encryption key refers to the password as
   "user password". I added a call to algorithm 11 to check if the
   password is the user password that isn't in the spec. Maybe I'm
   misunderstanding the spec, but this looks like a spec bug to me.

3. It says "using AES-256 in ECB mode with an initialization vector
   of zero". ECB mode has no initialization vector. CBC mode with
   initialization vector of zero for message length 16 is the same
   as ECB mode though, so maybe that's meant? (In addition to the
   spec being a bit wobbly, using EBC in new software isn't
   recommended, but too late for that.)

SASLprep / stringprep still aren't implemented. For ASCII passwords
(including the important empty password), this is good enough.
This commit is contained in:
Nico Weber 2023-07-19 21:28:25 -04:00 committed by Andreas Kling
parent f8a3022ca2
commit 0428308420
2 changed files with 51 additions and 13 deletions

View file

@ -449,11 +449,13 @@ ByteBuffer StandardSecurityHandler::compute_encryption_key_r2_to_r5(ByteBuffer p
return encryption_key;
}
ByteBuffer StandardSecurityHandler::compute_encryption_key_r6_and_later(ByteBuffer password_string)
bool StandardSecurityHandler::compute_encryption_key_r6_and_later(ByteBuffer password_string)
{
// This function should never be called after we have a valid encryption key.
VERIFY(!m_encryption_key.has_value());
auto const zero_iv = ByteBuffer::create_zeroed(16).release_value_but_fixme_should_propagate_errors();
// ISO 32000 (PDF 2.0), 7.6.4.3.3 Algorithm 2.A: Retrieving the file encryption key from an encrypted
// document in order to decrypt it (revision 6 or later)
@ -475,24 +477,60 @@ ByteBuffer StandardSecurityHandler::compute_encryption_key_r6_and_later(ByteBuff
// consisting of the UTF-8 password concatenated with the 8 bytes of owner Validation Salt, concatenated
// with the 48-byte U string. If the 32-byte result matches the first 32 bytes of the O string, this is the owner
// password.
// [Implementor's note: This is the same as Algorithm 12 in the spec.]
if (authenticate_owner_password_r6_and_later(password_string)) {
// d) Compute an intermediate owner key by computing a hash using algorithm 2.B with an input string
// consisting of the UTF-8 owner password concatenated with the 8 bytes of owner Key Salt, concatenated
// with the 48-byte U string. The 32-byte result is the key used to decrypt the 32-byte OE string using AES-
// 256 in CBC mode with no padding and an initialization vector of zero. The 32-byte result is the file
// encryption key.
ByteBuffer input;
input.append(password_string);
input.append(m_o_entry.bytes().slice(40, 8));
input.append(m_u_entry.bytes());
auto key = computing_a_hash_r6_and_later(input, password_string, HashKind::Owner);
// d) Compute an intermediate owner key by computing a hash using algorithm 2.B with an input string
// consisting of the UTF-8 owner password concatenated with the 8 bytes of owner Key Salt, concatenated
// with the 48-byte U string. The 32-byte result is the key used to decrypt the 32-byte OE string using AES-
// 256 in CBC mode with no padding and an initialization vector of zero. The 32-byte result is the file
// encryption key.
// [Implementor's note: PaddingMode doesn't matter here since input is block-aligned.]
auto cipher = Crypto::Cipher::AESCipher::CBCMode(key, 256, Crypto::Cipher::Intent::Decryption, Crypto::Cipher::PaddingMode::Null);
auto decrypted = cipher.create_aligned_buffer(m_oe_entry.length()).release_value_but_fixme_should_propagate_errors();
Bytes decrypted_span = decrypted.bytes();
cipher.decrypt(m_oe_entry.bytes(), decrypted_span, zero_iv);
m_encryption_key = ByteBuffer::copy(decrypted_span).release_value_but_fixme_should_propagate_errors();
}
// [Implementor's note: The spec seems to miss a step like c) but for the user password here.]
else if (authenticate_user_password_r6_and_later(password_string)) {
// e) Compute an intermediate user key by computing a hash using algorithm 2.B with an input string
// consisting of the UTF-8 user password concatenated with the 8 bytes of user Key Salt. The 32-byte result
// is the key used to decrypt the 32-byte UE string using AES-256 in CBC mode with no padding and an
// initialization vector of zero. The 32-byte result is the file encryption key.
ByteBuffer input;
input.append(password_string);
input.append(m_u_entry.bytes().slice(40, 8));
auto key = computing_a_hash_r6_and_later(input, password_string, HashKind::User);
// e) Compute an intermediate user key by computing a hash using algorithm 2.B with an input string
// consisting of the UTF-8 user password concatenated with the 8 bytes of user Key Salt. The 32-byte result
// is the key used to decrypt the 32-byte UE string using AES-256 in CBC mode with no padding and an
// initialization vector of zero. The 32-byte result is the file encryption key.
// [Implementor's note: PaddingMode doesn't matter here since input is block-aligned.]
auto cipher = Crypto::Cipher::AESCipher::CBCMode(key, 256, Crypto::Cipher::Intent::Decryption, Crypto::Cipher::PaddingMode::Null);
auto decrypted = cipher.create_aligned_buffer(m_ue_entry.length()).release_value_but_fixme_should_propagate_errors();
Bytes decrypted_span = decrypted.bytes();
cipher.decrypt(m_ue_entry.bytes(), decrypted_span, zero_iv);
m_encryption_key = ByteBuffer::copy(decrypted_span).release_value_but_fixme_should_propagate_errors();
}
// [Implementor's note: No explicit step for this in the spec, but if we get here the password was neither owner nor user password.]
else {
return false;
}
// f) Decrypt the 16-bye Perms string using AES-256 in ECB mode with an initialization vector of zero and
// f) Decrypt the 16-byte Perms string using AES-256 in ECB mode with an initialization vector of zero and
// the file encryption key as the key. Verify that bytes 9-11 of the result are the characters "a", "d", "b". Bytes
// 0-3 of the decrypted Perms entry, treated as a little-endian integer, are the user permissions. They shall
// match the value in the P key.
// [Implementor's note: For 16-byte long messages, CBC with an IV of zero is the same as ECB. ECB with an IV doesn't make a lot of sense (?) Maybe the spec means CBC.]
auto cipher = Crypto::Cipher::AESCipher::CBCMode(m_encryption_key.value(), 256, Crypto::Cipher::Intent::Decryption, Crypto::Cipher::PaddingMode::Null);
auto decrypted = cipher.create_aligned_buffer(m_perms_entry.length()).release_value_but_fixme_should_propagate_errors();
Bytes decrypted_span = decrypted.bytes();
cipher.decrypt(m_perms_entry.bytes(), decrypted_span, zero_iv);
TODO();
return decrypted_span[9] == 'a' && decrypted_span[10] == 'd' && decrypted_span[11] == 'b' && *bit_cast<LittleEndian<u32>*>(decrypted_span.data()) == m_flags;
}
ByteBuffer StandardSecurityHandler::computing_a_hash_r6_and_later(ByteBuffer original_input, StringView input_password, HashKind kind)

View file

@ -62,7 +62,7 @@ private:
bool authenticate_owner_password_r6_and_later(StringView password_string);
ByteBuffer compute_encryption_key_r2_to_r5(ByteBuffer password_string);
ByteBuffer compute_encryption_key_r6_and_later(ByteBuffer password_string);
bool compute_encryption_key_r6_and_later(ByteBuffer password_string);
enum class HashKind {
Owner,