The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Spreadsheet::ParseXLSX::Decryptor;
our $AUTHORITY = 'cpan:DOY';
$Spreadsheet::ParseXLSX::Decryptor::VERSION = '0.25';
use strict;
use warnings;

use Crypt::Mode::CBC;
use Crypt::Mode::ECB;
use Digest::SHA ();
use Encode ();
use File::Temp ();
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 ($infoFH, $packageFH) = $class->_getCompoundData(
        $filename,
        ['EncryptionInfo', 'EncryptedPackage']
    );

    return unless $infoFH;

    my $buffer;
    $infoFH->read($buffer, 8);
    my ($majorVers, $minorVers) = unpack('s<s<', $buffer);

    my $xlsx;
    if ($majorVers == 4 && $minorVers == 4) {
        $xlsx = $class->_agileDecryption($infoFH, $packageFH, $password);
    } else {
        $xlsx = $class->_standardDecryption($infoFH, $packageFH, $password);
    }

    return $xlsx;
}

sub _getCompoundData {
    my $class = shift;
    my ($filename, $names) = @_;

    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 = File::Temp->new;
            binmode($fh);
            $fh->write($data[0]->{Data});
            $fh->seek(0, 0);
            push @files, $fh;
        }
    }

    return @files;
}

sub _standardDecryption {
    my $class = shift;
    my ($infoFH, $packageFH, $password) = @_;

    my $buffer;
    my $n = $infoFH->read($buffer, 24);

    my ($encryptionHeaderSize, undef, undef, $algID, $algIDHash, $keyBits) = unpack('L<*', $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 $fh = File::Temp->new;
    binmode($fh);

    my $inbuf;
    $packageFH->read($inbuf, 8);
    my $fileSize = unpack('L<', $inbuf);

    $decryptor->decryptFile($packageFH, $fh, 1024, $fileSize);

    $fh->seek(0, 0);

    return $fh;
}

sub _agileDecryption {
    my $class = shift;
    my ($infoFH, $packageFH, $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 $fh = File::Temp->new;
    binmode($fh);

    my $inbuf;
    $packageFH->read($inbuf, 8);
    my $fileSize = unpack('L<', $inbuf);

    $fileDecryptor->decryptFile($packageFH, $fh, 4096, $key, $fileSize);

    $fh->seek(0, 0);

    return $fh;
}

sub new {
    my $class = shift;
    my ($args) = @_;

    my $self = { %$args };
    $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;

__END__

=pod

=encoding UTF-8

=head1 NAME

Spreadsheet::ParseXLSX::Decryptor

=head1 VERSION

version 0.25

=for Pod::Coverage   new
  open

=head1 AUTHOR

Jesse Luehrs <doy@tozt.net>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2016 by Jesse Luehrs.

This is free software, licensed under:

  The MIT (X11) License

=cut