AWS-Signature-V4

 view release on metacpan or  search on metacpan

lib/AWS/Signature/V4/Chunker.pm  view on Meta::CPAN

package AWS::Signature::V4::Chunker;
use v5.24;
use Moo;
use AWS::Signature::V4::Error qw< fail shown >;
use Digest::SHA qw< sha256_hex hmac_sha256_hex >;
use experimental qw< signatures >;
use namespace::clean;

use AWS::Signature::V4::Checksum ();

# The chunker is created by AWS::Signature::V4::sign(), which knows all of
# this: the arguments are not a public interface.
has signed        => (is => 'ro', required => 1);
has amzdate       => (is => 'ro');
has scope         => (is => 'ro');

# The signing key, for signed chunks only, can sign any request for the same
# service, region and day: it is kept in a closure, so that no accessor gives
# it back and dumping the object does not show it. The closure can still sign
# with it, so the chunker is no safer to share than the signer.
has _mac => (
   is       => 'ro',
   init_arg => 'key',
   coerce   => sub ($key) {
      ref $key eq 'CODE' ? $key : sub ($data) { hmac_sha256_hex($data, $key) };
   },
);

# signature of the previous chunk: the chain must not be changed from outside
has _previous => (is => 'rw', init_arg => 'previous');
has expected      => (is => 'ro', required => 1);    # declared size of the data
has checksum      => (is => 'ro');    # name of the built-in checksum, if any
has trailer_names => (is => 'ro', default => sub { [] });

has _seen => (is => 'rw', init_arg => undef, default => 0);
has _done => (is => 'rw', init_arg => undef, default => 0);
has _sum  => (is => 'lazy', init_arg => undef);

sub BUILD ($self, $args) {
   fail 500, 'signed chunks need a "key"' if $self->signed && !$self->_mac;
   return;
}

sub _build__sum ($self) {
   my $algo = $self->checksum;
   return defined $algo ? AWS::Signature::V4::Checksum->new($algo) : undef;
}

sub _sign ($self, $data_ref) {
   my $string_to_sign = join "\n", 'AWS4-HMAC-SHA256-PAYLOAD',
      $self->amzdate, $self->scope, $self->_previous,
      sha256_hex(''), sha256_hex($$data_ref);
   return $self->_previous($self->_mac->($string_to_sign));
}

# encoded chunk for the data (a byte string or a reference to one)
sub chunk ($self, $data) {
   fail 400, 'chunker already finished' if $self->_done;
   fail 400, 'chunk must be a byte string or a reference to one'
      if ref $data && ref $data ne 'SCALAR';
   my $ref = ref $data ? $data : \$data;
   my $len = length($$ref) // 0;
   fail 400, 'empty chunk: call finish() to terminate the stream' unless $len;
   _is_bytes($ref) or fail 400, 'chunk must be a byte string: encode characters first';
   my $seen = $self->_seen + $len;
   fail 400, "more data than the declared @{[ $self->expected ]} bytes"
      if $seen > $self->expected;
   $self->_seen($seen);
   $self->_sum->add($ref) if $self->_sum;
   my $head = $self->signed
      ? sprintf('%x;chunk-signature=%s', $len, $self->_sign($ref))
      : sprintf('%x', $len);
   return "$head\r\n" . $$ref . "\r\n";
}

# the final, empty chunk, followed by the trailers if any; the values of the
# trailers that were declared with "trailers" are passed by name, in any case
sub finish ($self, %given) {
   fail 400, 'chunker already finished' if $self->_done;
   fail 400, "got @{[ $self->_seen ]} bytes instead of the declared @{[ $self->expected ]}"
      if $self->_seen != $self->expected;
   my %values;
   for my $name (sort keys %given) {
      fail 400, "trailer '" . shown(lc $name) . "' given twice" if exists $values{lc $name};
      $values{lc $name} = $given{$name};
   }

   # all checks come before the checksum is taken, which is not repeatable,
   # and before the chain of signatures moves on: after an error, the chunker
   # is as it was
   my $computed = $self->_sum ? 'x-amz-checksum-' . $self->checksum : undef;
   my @pairs;
   for my $name ($self->trailer_names->@*) {
      my $value;
      if (defined $computed && $name eq $computed) {
         fail 400, "trailer '$name' is computed, not to be given" if exists $values{$name};
      }
      else {
         $value = delete $values{$name} // fail 400, "missing value for trailer '$name'";
         fail 400, "invalid value for trailer '$name': a byte string, no CR, LF or NUL"
            if ref $value || $value =~ m{[\r\n\0]} || !_is_bytes(\$value);
         utf8::downgrade($value);
      }
      push @pairs, [$name, $value];
   }
   fail 400, 'undeclared trailers: ' . join(', ', map { shown($_) } sort keys %values)
      if %values;
   my @lines = map { "$_->[0]:" . ($_->[1] // $self->_sum->base64) } @pairs;
   my $trailers = join '', map { "$_\r\n" } @lines;

   my $body;
   if (!$self->signed) {
      $body = "0\r\n$trailers\r\n";
   }
   elsif (!@lines) {
      $body = "0;chunk-signature=@{[ $self->_sign(\'') ]}\r\n\r\n";
   }
   else {
      my $sig = $self->_sign(\'');
      my $string_to_sign = join "\n", 'AWS4-HMAC-SHA256-TRAILER',
         $self->amzdate, $self->scope, $self->_previous,
         sha256_hex(join '', map { "$_\n" } @lines);
      my $tsig = $self->_mac->($string_to_sign);
      $body = "0;chunk-signature=$sig\r\n${trailers}x-amz-trailer-signature:$tsig\r\n\r\n";
   }
   $self->_done(1);



( run in 1.493 second using v1.01-cache-2.11-cpan-16aabcbfbcb )