diff options
author | Jesse Luehrs <doy@tozt.net> | 2015-12-04 00:46:58 -0500 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2015-12-04 00:52:50 -0500 |
commit | f1f3c514a88f9577b20a5166ccf5a801a9acb614 (patch) | |
tree | 70b1f8f9a0c48909dfe0ea2f537ddc1fff9d0c16 /lib/Spreadsheet/ParseXLSX | |
parent | 963983123e5166b3e8bead9ed8d7d6ac175c1cb1 (diff) | |
download | spreadsheet-parsexlsx-f1f3c514a88f9577b20a5166ccf5a801a9acb614.tar.gz spreadsheet-parsexlsx-f1f3c514a88f9577b20a5166ccf5a801a9acb614.zip |
split the decryptor modules out into their own files
Diffstat (limited to 'lib/Spreadsheet/ParseXLSX')
-rw-r--r-- | lib/Spreadsheet/ParseXLSX/Decryptor.pm | 222 | ||||
-rw-r--r-- | lib/Spreadsheet/ParseXLSX/Decryptor/Agile.pm | 107 | ||||
-rw-r--r-- | lib/Spreadsheet/ParseXLSX/Decryptor/Standard.pm | 87 |
3 files changed, 416 insertions, 0 deletions
diff --git a/lib/Spreadsheet/ParseXLSX/Decryptor.pm b/lib/Spreadsheet/ParseXLSX/Decryptor.pm new file mode 100644 index 0000000..e98425f --- /dev/null +++ b/lib/Spreadsheet/ParseXLSX/Decryptor.pm @@ -0,0 +1,222 @@ +package Spreadsheet::ParseXLSX::Decryptor; +use strict; +use warnings; + +use Crypt::Mode::CBC; +use Crypt::Mode::ECB; +use Digest::SHA (); +use Encode (); +use File::Temp 'tempfile'; +use MIME::Base64 (); +use OLE::Storage_Lite; + +use Spreadsheet::ParseXLSX::Decryptor::Standard; +use Spreadsheet::ParseXLSX::Decryptor::Agile; + +sub open { + my $class = shift; + + my ($filename, $password) = @_; + + $password = $password || 'VelvetSweatshop'; + + my ($infoFile, $packageFile) = _getCompoundData($filename, ['EncryptionInfo', 'EncryptedPackage']); + + my $xlsx; + + eval { + my $infoFH = IO::File->new(); + $infoFH->open($infoFile); + $infoFH->binmode(); + + my $buffer; + $infoFH->read($buffer, 8); + my ($majorVers, $minorVers) = unpack('SS', $buffer); + + if ($majorVers == 4 && $minorVers == 4) { + $xlsx = agileDecryption($infoFH, $packageFile, $password); + } else { + $xlsx = standardDecryption($infoFH, $packageFile, $password); + } + $infoFH->close(); + }; + unlink $infoFile, $packageFile; + die $@ if $@; + + return $xlsx; +} + +sub _getCompoundData { + my $filename = shift; + my $names = shift; + + my @files; + + my $storage = OLE::Storage_Lite->new($filename); + + foreach my $name (@{$names}) { + my @data = $storage->getPpsSearch([OLE::Storage_Lite::Asc2Ucs($name)], 1, 1); + if ($#data < 0) { + push @files, undef; + } else { + my ($fh, $filename) = File::Temp::tempfile(); + my $out = IO::Handle->new_from_fd($fh, 'w') || die "TempFile error!"; + $out->write($data[0]->{Data}); + $out->close(); + push @files, $filename; + } + } + + return @files; +} + +sub standardDecryption { + my ($infoFH, $packageFile, $password) = @_; + + my $buffer; + my $n = $infoFH->read($buffer, 24); + + my ($encryptionHeaderSize, undef, undef, $algID, $algIDHash, $keyBits) = unpack('LLLLLL', $buffer); + + $infoFH->seek($encryptionHeaderSize - 0x14, IO::File::SEEK_CUR); + + $infoFH->read($buffer, 4); + + my $saltSize = unpack('L', $buffer); + + my ($salt, $encryptedVerifier, $verifierHashSize, $encryptedVerifierHash); + + $infoFH->read($salt, 16); + $infoFH->read($encryptedVerifier, 16); + + $infoFH->read($buffer, 4); + $verifierHashSize = unpack('L', $buffer); + + $infoFH->read($encryptedVerifierHash, 32); + $infoFH->close(); + + my ($cipherAlgorithm, $hashAlgorithm); + + if ($algID == 0x0000660E || $algID == 0x0000660F || $algID == 0x0000660E) { + $cipherAlgorithm = 'AES'; + } else { + die sprintf('Unsupported encryption algorithm: 0x%.8x', $algID); + } + + if ($algIDHash == 0x00008004) { + $hashAlgorithm = 'SHA-1'; + } else { + die sprintf('Unsupported hash algorithm: 0x%.8x', $algIDHash); + } + + my $decryptor = Spreadsheet::ParseXLSX::Decryptor::Standard->new({ + cipherAlgorithm => $cipherAlgorithm, + cipherChaining => 'ECB', + hashAlgorithm => $hashAlgorithm, + salt => $salt, + password => $password, + keyBits => $keyBits, + spinCount => 50000 + }); + + $decryptor->verifyPassword($encryptedVerifier, $encryptedVerifierHash); + + my $in = new IO::File; + $in->open("<$packageFile") || die 'File/handle opening error'; + $in->binmode(); + + my ($fh, $filename) = File::Temp::tempfile(); + binmode($fh); + my $out = IO::Handle->new_from_fd($fh, 'w') || die "TempFile error!"; + + my $inbuf; + $in->read($inbuf, 8); + my $fileSize = unpack('L', $inbuf); + + $decryptor->decryptFile($in, $out, 1024, $fileSize); + + $in->close(); + $out->close(); + + return $filename; +} + +sub agileDecryption { + my ($infoFH, $packageFile, $password) = @_; + + my $xml = XML::Twig->new; + $xml->parse($infoFH); + + my ($info) = $xml->find_nodes('//encryption/keyEncryptors/keyEncryptor/p:encryptedKey'); + + my $encryptedVerifierHashInput = MIME::Base64::decode($info->att('encryptedVerifierHashInput')); + my $encryptedVerifierHashValue = MIME::Base64::decode($info->att('encryptedVerifierHashValue')); + my $encryptedKeyValue = MIME::Base64::decode($info->att('encryptedKeyValue')); + + my $keyDecryptor = Spreadsheet::ParseXLSX::Decryptor::Agile->new({ + cipherAlgorithm => $info->att('cipherAlgorithm'), + cipherChaining => $info->att('cipherChaining'), + hashAlgorithm => $info->att('hashAlgorithm'), + salt => MIME::Base64::decode($info->att('saltValue')), + password => $password, + keyBits => 0 + $info->att('keyBits'), + spinCount => 0 + $info->att('spinCount'), + blockSize => 0 + $info->att('blockSize') + }); + + $keyDecryptor->verifyPassword($encryptedVerifierHashInput, $encryptedVerifierHashValue); + + my $key = $keyDecryptor->decrypt($encryptedKeyValue, "\x14\x6e\x0b\xe7\xab\xac\xd0\xd6"); + + ($info) = $xml->find_nodes('//encryption/keyData'); + + my $fileDecryptor = Spreadsheet::ParseXLSX::Decryptor::Agile->new({ + cipherAlgorithm => $info->att('cipherAlgorithm'), + cipherChaining => $info->att('cipherChaining'), + hashAlgorithm => $info->att('hashAlgorithm'), + salt => MIME::Base64::decode($info->att('saltValue')), + password => $password, + keyBits => 0 + $info->att('keyBits'), + blockSize => 0 + $info->att('blockSize') + }); + + my $in = new IO::File; + $in->open("<$packageFile") || die 'File/handle opening error'; + $in->binmode(); + + my ($fh, $filename) = File::Temp::tempfile(); + binmode($fh); + my $out = IO::Handle->new_from_fd($fh, 'w') || die "TempFile error!"; + + my $inbuf; + $in->read($inbuf, 8); + my $fileSize = unpack('L', $inbuf); + + $fileDecryptor->decryptFile($in, $out, 4096, $key, $fileSize); + + $in->close(); + $out->close(); + + return $filename; +} + +sub new { + my $class = shift; + my $self = shift; + + $self->{keyLength} = $self->{keyBits} / 8; + + if ($self->{hashAlgorithm} eq 'SHA512') { + $self->{hashProc} = \&Digest::SHA::sha512; + } elsif ($self->{hashAlgorithm} eq 'SHA-1') { + $self->{hashProc} = \&Digest::SHA::sha1; + } elsif ($self->{hashAlgorithm} eq 'SHA256') { + $self->{hashProc} = \&Digest::SHA::sha256; + } else { + die "Unsupported hash algorithm: $self->{hashAlgorithm}"; + } + + return bless $self, $class; +} + +1; diff --git a/lib/Spreadsheet/ParseXLSX/Decryptor/Agile.pm b/lib/Spreadsheet/ParseXLSX/Decryptor/Agile.pm new file mode 100644 index 0000000..3b836e3 --- /dev/null +++ b/lib/Spreadsheet/ParseXLSX/Decryptor/Agile.pm @@ -0,0 +1,107 @@ +package Spreadsheet::ParseXLSX::Decryptor::Agile; +use strict; +use warnings; + +use base 'Spreadsheet::ParseXLSX::Decryptor'; + +sub new { + my $class = shift; + my $self = Spreadsheet::ParseXLSX::Decryptor->new(@_); + bless $self, $class; +} + +sub decrypt { + my $self = shift; + my ($encryptedValue, $blockKey) = @_; + + my $key = $self->_generateDecryptionKey($blockKey); + my $iv = $self->_generateInitializationVector('', $self->{blockSize}); + my $cbc = Crypt::Mode::CBC->new($self->{cipherAlgorithm}, 0); + return $cbc->decrypt($encryptedValue, $key, $iv); +} + +sub _generateDecryptionKey { + my $self = shift; + my ($blockKey) = @_; + + my $hash; + + unless ($self->{pregeneratedKey}) { + $hash = $self->{hashProc}->($self->{salt} . Encode::encode('UTF-16LE', $self->{password})); + for (my $i = 0; $i < $self->{spinCount}; $i++) { + $hash = $self->{hashProc}->(pack('L', $i) . $hash); + } + $self->{pregeneratedKey} = $hash; + } + + $hash = $self->{hashProc}->($self->{pregeneratedKey} . $blockKey); + + if (length($hash) > $self->{keyLength}) { + $hash = substr($hash, 0, $self->{keyLength}); + } elsif (length($hash) < $self->{keyLength}) { + $hash .= "\x36" x ($self->{keyLength} - length($hash)); + } + return $hash; +} + +sub _generateInitializationVector { + my $self = shift; + my ($blockKey, $blockSize) = @_; + + my $iv; + if ($blockKey) { + $iv = $self->{hashProc}->($self->{salt} . $blockKey); + } else { + $iv = $self->{salt}; + } + + if (length($iv) > $blockSize) { + $iv = substr($iv, 0, $blockSize); + } elsif (length($iv) < $blockSize) { + $iv = $iv . ("\x36" x ($blockSize - length($iv))); + } + + return $iv; +} + +sub decryptFile { + my $self = shift; + my ($inFile, $outFile, $bufferLength, $key, $fileSize) = @_; + + my $cbc = Crypt::Mode::CBC->new($self->{cipherAlgorithm}, 0); + + my $inbuf; + my $i = 0; + + while (($fileSize > 0) && (my $inlen = $inFile->read($inbuf, $bufferLength))) { + my $blockId = pack('L', $i); + + my $iv = $self->_generateInitializationVector($blockId, $self->{blockSize}); + + if ($inlen < $bufferLength) { + $inbuf .= "\x00" x ($bufferLength - $inlen); + } + + my $outbuf = $cbc->decrypt($inbuf, $key, $iv); + if ($fileSize < $inlen) { + $inlen = $fileSize; + } + + $outFile->write($outbuf, $inlen); + $i++; + $fileSize -= $inlen; + } +} + +sub verifyPassword { + my $self = shift; + + my ($encryptedVerifier, $encryptedVerifierHash) = @_; + + my $encryptedVerifierHash0 = $self->{hashProc}->($self->decrypt($encryptedVerifier, "\xfe\xa7\xd2\x76\x3b\x4b\x9e\x79")); + $encryptedVerifierHash = $self->decrypt($encryptedVerifierHash, "\xd7\xaa\x0f\x6d\x30\x61\x34\x4e"); + + die "Wrong password: $self" unless ($encryptedVerifierHash0 eq $encryptedVerifierHash); +} + +1; diff --git a/lib/Spreadsheet/ParseXLSX/Decryptor/Standard.pm b/lib/Spreadsheet/ParseXLSX/Decryptor/Standard.pm new file mode 100644 index 0000000..a940719 --- /dev/null +++ b/lib/Spreadsheet/ParseXLSX/Decryptor/Standard.pm @@ -0,0 +1,87 @@ +package Spreadsheet::ParseXLSX::Decryptor::Standard; +use strict; +use warnings; + +use base 'Spreadsheet::ParseXLSX::Decryptor'; + +sub new { + my $class = shift; + my $self = Spreadsheet::ParseXLSX::Decryptor->new(@_); + bless $self, $class; +} + +sub decrypt { + my $self = shift; + my ($encryptedValue) = @_; + + my $key = $self->_generateDecryptionKey("\x00" x 4); + my $ecb = Crypt::Mode::ECB->new($self->{cipherAlgorithm}, 0); + return $ecb->decrypt($encryptedValue, $key); +} + +sub decryptFile { + my $self = shift; + my ($inFile, $outFile, $bufferLength, $fileSize) = @_; + + my $key = $self->_generateDecryptionKey("\x00" x 4); + my $ecb = Crypt::Mode::ECB->new($self->{cipherAlgorithm}, 0); + + my $inbuf; + my $i = 0; + + while (($fileSize > 0) && (my $inlen = $inFile->read($inbuf, $bufferLength))) { + if ($inlen < $bufferLength) { + $inbuf .= "\x00" x ($bufferLength - $inlen); + } + + my $outbuf = $ecb->decrypt($inbuf, $key); + if ($fileSize < $inlen) { + $inlen = $fileSize; + } + + $outFile->write($outbuf, $inlen); + $i++; + $fileSize -= $inlen; + } +} + +sub _generateDecryptionKey { + my $self = shift; + my ($blockKey) = @_; + + my $hash; + unless ($self->{pregeneratedKey}) { + $hash = $self->{hashProc}->($self->{salt} . Encode::encode('UTF-16LE', $self->{password})); + for (my $i = 0; $i < $self->{spinCount}; $i++) { + $hash = $self->{hashProc}->(pack('L', $i) . $hash); + } + $self->{pregeneratedKey} = $hash; + } + + $hash = $self->{hashProc}->($self->{pregeneratedKey} . $blockKey); + + my $x1 = $self->{hashProc}->(("\x36" x 64) ^ $hash); + if (length($x1) >= $self->{keyLength}) { + $hash = substr($x1, 0, $self->{keyLength}); + } else { + my $x2 = $self->{hashProc}->(("\x5C" x 64) ^ $hash); + $hash = substr($x1 . $x2, 0, $self->{keyLength}); + } + + return $hash; +} + +sub verifyPassword { + my $self = shift; + + my ($encryptedVerifier, $encryptedVerifierHash) = @_; + + my $verifier = $self->decrypt($encryptedVerifier); + my $verifierHash = $self->decrypt($encryptedVerifierHash); + + my $verifierHash0 = $self->{hashProc}->($verifier); + + die "Wrong password: $self" unless ($verifierHash0 eq substr($verifierHash, 0, length($verifierHash0))); +} + +1; |