getUserPasswordEntry(); $securityHandlerRevision = $encryptDictionary->getStandardSecurityHandlerRevision(); $fileEncryptionKey = $this->getUserFileEncryptionKey($encryptDictionary, $firstID); if ($securityHandlerRevision === StandardSecurityHandlerRevision::v2) { // @see 7.6.4.4.3, step b return hash_equals($userPasswordEntry, RC4::crypt($fileEncryptionKey, self::PADDING_STRING)); } if (in_array($securityHandlerRevision, [StandardSecurityHandlerRevision::v3, StandardSecurityHandlerRevision::v4], true)) { // @see 7.6.4.4.4, step b through e $hash = md5(self::PADDING_STRING . $firstID, true); $encryptedHash = RC4::crypt($fileEncryptionKey, $hash); for ($i = 1; $i <= 19; $i++) { $encryptedHash = RC4::crypt( implode('', array_map( fn ($c) => chr(ord($c) ^ $i), str_split($fileEncryptionKey) )), $encryptedHash, ); } return hash_equals(substr($userPasswordEntry, 0, 16), $encryptedHash); } throw new NotImplementedException('Unsupported security handler revision: ' . $securityHandlerRevision->value); } /** @see 7.6.4.4.6 */ public function isOwnerPasswordValid(EncryptDictionary $encryptDictionary, string $firstID): bool { $fileEncryptionKey = $this->getOwnerFileEncryptionKey($encryptDictionary); $ownerPasswordEntry = $encryptDictionary->getOwnerPasswordEntry(); if ($encryptDictionary->getStandardSecurityHandlerRevision() === StandardSecurityHandlerRevision::v2) { $userPassword = RC4::crypt($fileEncryptionKey, $ownerPasswordEntry); } else { $userPassword = $ownerPasswordEntry; for ($i = 19; $i >= 0; $i--) { $userPassword = RC4::crypt( implode('', array_map( fn ($c) => chr(ord($c) ^ $i), str_split($fileEncryptionKey) )), $userPassword, ); } } if ($this->userPassword !== null && $userPassword !== $this->userPassword) { return false; } $this->userPassword = $userPassword; return $this->isUserPasswordValid($encryptDictionary, $firstID); } /** @see 7.6.4.4.2 */ public function getUserFileEncryptionKey(EncryptDictionary $encryptDictionary, string $firstIDValue): string { if (in_array($encryptDictionary->getStandardSecurityHandlerRevision(), [StandardSecurityHandlerRevision::v2, StandardSecurityHandlerRevision::v3, StandardSecurityHandlerRevision::v4], true) === false) { throw new NotImplementedException('Unsupported security handler revision: ' . $encryptDictionary->getStandardSecurityHandlerRevision()->value); } $fileEncryptionKeyLengthInBits = $encryptDictionary->getLengthFileEncryptionKeyInBits() ?? throw new ParseFailureException(); if ($encryptDictionary->getSecurityAlgorithm() === SecurityAlgorithm::AES_Key_length_256) { // V = 4 throw new NotImplementedException('AES-based stream decryption is not yet supported.'); } if ($fileEncryptionKeyLengthInBits % 8 !== 0 || !is_int($fileEncryptionKeyLengthInBytes = $fileEncryptionKeyLengthInBits / 8)) { throw new ParseFailureException('Unsupported file encryption key length in bits: ' . $fileEncryptionKeyLengthInBits); } $hashedString = $this->getPaddedUserPassword() // step a+b . $encryptDictionary->getOwnerPasswordEntry() // step c . pack('V', $encryptDictionary->getPValue()) // step d . $firstIDValue; // step e if ($encryptDictionary->getStandardSecurityHandlerRevision()->value >= 4 && $encryptDictionary->isMetadataEncrypted() === false) { $hashedString .= "\xFF\xFF\xFF\xFF"; } $md5Hash = md5($hashedString, true); if ($encryptDictionary->getStandardSecurityHandlerRevision() === StandardSecurityHandlerRevision::v2) { return substr($md5Hash, 0, 5); } for ($i = 1; $i <= 50; $i++) { // step h $md5Hash = md5(substr($md5Hash, 0, $fileEncryptionKeyLengthInBytes), true); } return substr($md5Hash, 0, $fileEncryptionKeyLengthInBytes); } private function getOwnerFileEncryptionKey(EncryptDictionary $encryptDictionary): string { if (in_array($encryptDictionary->getStandardSecurityHandlerRevision(), [StandardSecurityHandlerRevision::v2, StandardSecurityHandlerRevision::v3, StandardSecurityHandlerRevision::v4], true) === false) { throw new NotImplementedException('Unsupported security handler revision: ' . $encryptDictionary->getStandardSecurityHandlerRevision()->value); } $fileEncryptionKeyLengthInBits = $encryptDictionary->getLengthFileEncryptionKeyInBits() ?? throw new ParseFailureException(); if ($encryptDictionary->getSecurityAlgorithm() === SecurityAlgorithm::AES_Key_length_256) { // V = 4 throw new NotImplementedException('AES-based stream decryption is not yet supported.'); } if ($fileEncryptionKeyLengthInBits % 8 !== 0 || !is_int($fileEncryptionKeyLengthInBytes = $fileEncryptionKeyLengthInBits / 8)) { throw new ParseFailureException('Unsupported file encryption key length in bits: ' . $fileEncryptionKeyLengthInBits); } $md5Hash = md5($this->getPaddedOwnerPassword(), true); if ($encryptDictionary->getStandardSecurityHandlerRevision() !== StandardSecurityHandlerRevision::v2) { for ($i = 1; $i <= 50; $i++) { // step c $md5Hash = md5($md5Hash, true); } } if ($encryptDictionary->getStandardSecurityHandlerRevision() === StandardSecurityHandlerRevision::v2) { return substr($md5Hash, 0, 5); } return substr($md5Hash, 0, $fileEncryptionKeyLengthInBytes); } /** @see 7.6.4.3.2 step a */ public function getPaddedUserPassword(): string { return substr($this->userPassword ?? '', 0, self::PASSWORD_LENGTH) . substr(self::PADDING_STRING, 0, max(0, self::PASSWORD_LENGTH - strlen($this->userPassword ?? ''))); } /** @see 7.6.4.3.2 step a */ public function getPaddedOwnerPassword(): string { return substr($this->ownerPassword ?? $this->userPassword ?? '', 0, self::PASSWORD_LENGTH) . substr(self::PADDING_STRING, 0, max(0, self::PASSWORD_LENGTH - strlen($this->ownerPassword ?? $this->userPassword ?? ''))); } }