F4VDownloader: Add support for parsing DRM header and decrypting video

This is still a work in progress.  The decrypt and parser routines work OK,
but the downloader still cannot automatically retrieve the decryption key so
it currently has to be passed in by the site.  It should eventually be
possible to retrieve the key from the token.

Signed-off-by: James Bottomley <JBottomley@Parallels.com>
diff --git a/lib/FlashVideo/F4VDownloader.pm b/lib/FlashVideo/F4VDownloader.pm
index 1b7ce14..0e124c5 100644
--- a/lib/FlashVideo/F4VDownloader.pm
+++ b/lib/FlashVideo/F4VDownloader.pm
@@ -35,6 +35,136 @@
 use MIME::Base64;
 use Data::Dumper;
 use Data::AMF::Parser::AMF0;
+use Convert::ASN1;
+use Crypt::Rijndael;
+
+sub parse_audio_tag {
+    my ($s, $tag, $len) = @_;
+    my ($c) = unpack('C', $s);
+    my $soundformat = ($c >> 4);
+    my $hdrlen;
+
+    if ($soundformat == 10) {
+	$hdrlen = 2;
+    } else {
+	$hdrlen = 1;
+    }
+    return $hdrlen;
+}
+
+sub parse_video_tag {
+    my ($s, $tag, $len) = @_;
+    my ($c) = unpack('C', $s);
+    my $codec = ($c & 0x0f);
+    my $hdrlen;
+
+    if ($codec == 7) {
+	$hdrlen = 5;
+    } else {
+	$hdrlen = 1;
+    }
+    return $hdrlen;
+}
+
+my %parse_tag = (
+    8 => \&parse_audio_tag,
+    9 => \&parse_video_tag,
+);
+
+sub unpad {
+    my ($t) = @_;
+    my $c = substr($t, length($t)-1, 1);
+    my $pl = unpack('C', $c);
+    #info 'Pad length '.$pl;
+    return undef if ($pl == 0 || $pl > 16);
+    # verify
+    my $p = substr($t, length($t)-$pl, $pl);
+    return undef if ($p ne $c x $pl);
+    return substr($t, 0, length($t) - $pl);
+}
+
+sub parse_flv_tag {
+    my($s, $key) = @_;
+    my $data = '';
+    my($tag, $l1, $l2, $l3) = unpack('CCCC', $s);
+    my $len = ($l1 << 16) + ($l2 << 8) + $l3;
+    die sprintf("Invalid tag %02x ",$tag) if (($tag & 0xc0) != 0);
+    my $encrypted = $tag & 0x20;
+    my $type = $tag & 0x1f;
+    my $typedesc;
+    my $new = substr($s, 11); #skip past rest of header on to content headers
+    my $dat = substr($new, 0, $len);
+    die "Unrecognised tag $type" if (!defined($parse_tag{$type}));
+    debug "parsing tag $tag";
+    my $extralen = $parse_tag{$type}($dat, $tag, $len);
+    # invariant elements of the header (i.e. less tag type and length);
+    my $hdr = substr($s, 4, 11 - 4 + $extralen);
+    $s = $new;
+    $dat = substr($dat, $extralen);
+    my $dec = $dat;
+    if ($encrypted) {
+	my ($numfilter, $etype, $l1, $l2, $l3,$l) = unpack('CZ*CCC.*', $dat);
+	$dat = substr($dat, $l);
+	$l = ($l1 << 16) + ($l2 << 8) + $l3;
+	my $iv;
+	my $enc;
+	if ($etype eq 'SE') {
+	    ($enc) = unpack('C', $dat);
+	    die "Wrong encoding byte $enc" if ($enc & 0x7f != 0);
+	    $dat = substr($dat, 1);
+	    if ($enc) {
+		$iv = substr($dat, 0, 16);
+		$dat = substr($dat, 16);
+		die "Wrong filter length" if ($l != 17);
+	    } else {
+		die "Wrong filter length" if ($l != 1);
+	    }
+	} elsif ($etype eq 'Encryption') {
+	    $enc = 1;
+	    $iv = substr($dat, 0 ,16);
+	    $dat = substr($dat, 16);
+	    die "Wrong filter length" if ($l != 16);
+	} else {
+	    die 'Unknown encryption type '.$etype;
+	}
+	if ($enc) {
+	    my ($ivstr) = unpack('H*', $iv);
+	    $data .= ' iv:'.$ivstr;
+	} else {
+	    $data .= ' unencrypted';
+	}
+	debug 'data size after headers '.length($dat);
+	my $cipher = Crypt::Rijndael->new($key, Crypt::Rijndael::MODE_CBC());
+	$cipher->set_iv($iv);
+	$dec = unpad($cipher->decrypt($dat));
+	
+	die "Decryption error in tag" if (!defined($dec));
+	debug "size after decryption ".length($dec);
+    }
+    my $newlen = length($dec) + $extralen;
+    my $newhdr = pack('CCCC', $type, ($newlen >> 16) & 0xff,
+		      ($newlen >> 8) & 0xff, $newlen & 0xff);
+    my $newtrailer = pack('L>', $newlen + 11);
+    # bang on header to get decrypted tag
+    $dec = $newhdr.$hdr.$dec.$newtrailer;
+    debug sprintf('%s%s tag of length %d: %s', $encrypted ? "encrypted " : "", $typedesc, $len, $data);
+    $s = substr($s, $len);
+    my $prevlen = unpack('L>', $s);
+    die $len.' != '.$prevlen if (($len + 11) != $prevlen);
+    $s = substr($s, 4);
+    return ($s, $dec);
+}
+	
+sub flv_decrypt {
+    my ($dat, $key) = @_;
+    my $newdat = '';
+    while (length($dat) > 0) {
+	my $d;
+	($dat, $d) = parse_flv_tag($dat, $key);
+	$newdat .= $d;
+    }
+    return $newdat;
+}
 
 sub read_box_header {
 
@@ -222,6 +352,7 @@
   $self->{printable_filename} = $file;
 
   $file = $data->{flv} = $self->get_filename($file);
+  my $encryptionkey = $data->{encryptionkey};
 
   if (-s $file && !$data->{live}) {
     info "F4V output filename '$self->{printable_filename}' already " .
@@ -292,7 +423,7 @@
   # standard says this must be present, but it often isn't, sigh
   die "This download isn't streaming media" if (defined($manifest->{deliveryType}) && $manifest->{deliveryType} ne 'streaming');
 
-  die "F4VDownloader can't currently handle DRM encoded files" if (defined($manifest->{drmAdditionalHeader}));
+  #die "F4VDownloader can't currently handle DRM encoded files" if (defined($manifest->{drmAdditionalHeader}));
 
   # can't do http streaming without bootstrap information
   die ("manifest has no bootstrapbox") if (!defined($manifest->{bootstrapInfo}));
@@ -325,10 +456,168 @@
       }
   }
   info "Selected stream of resolution ".$media->{md}->{height}."x".$media->{md}->{width}." and bitrate ".$bitrate;
-  $media->{bootstrapInfo} = $manifest->{bootstrapInfo}->{$media->{bootstrapInfoId}};
+  if (defined($manifest->{bootstrapInfo}->{$media->{bootstrapInfoId}})) {
+      $media->{bootstrapInfo} = $manifest->{bootstrapInfo}->{$media->{bootstrapInfoId}};
+  } elsif ($manifest->{bootstrapInfo}->{id} eq $media->{bootstrapInfoId}) {
+      $media->{bootstrapInfo} = $manifest->{bootstrapInfo};
+  } else {
+      die "Can't find bootstrapID ".$media->{bootstrapInfoId};
+  }
+  my $drmmetadata;
+  if (defined($media->{drmAdditionalHeaderId})) {
+      info "DRM Additional Header ".$media->{drmAdditionalHeaderId};
+      my $drm;
+      if (defined($manifest->{drmAdditionalHeader}->{$media->{drmAdditionalHeaderId}})) {
+	  $drm = $manifest->{drmAdditionalHeader}->{$media->{drmAdditionalHeaderId}};
+      } elsif ($manifest->{drmAdditionalHeader}->{id} eq $media->{drmAdditionalHeaderId}) {
+	  $drm = $manifest->{drmAdditionalHeader};
+      } else {
+	  die "Can't find drmAdditionalHeader ".$media->{drmAdditionalHeaderId};
+      }
+      $drm = decode_base64($drm->{content});
+      @_ = Data::AMF::Parser::AMF0->parse($drm);
+      # skip header
+      $drm = $_[1];
+      if ($drm->{Encryption}->{Version} == 2) {
+	  $drmmetadata = decode_base64($drm->{Encryption}->{Params}->{KeyInfo}->{FMRMS_METADATA}->{Metadata});
+      } elsif ($drm->{Encryption}->{Version} == 2) {
+	  $drmmetadata = decode_base64($drm->{Encryption}->{Params}->{KeyInfo}->{Data}->{Metadata});
+      } else {
+	  die "Unknown Encryption Version ".$drm->{Encryption}->{Version};
+      }
+      my $asn1 = Convert::ASN1->new();
+      $asn1->configure(encoding => 'DER');
+      $asn1->prepare(<<ASN1);
+
+-- http://www.ietf.org/rfc/rfc2315.txt
+-- http://www.ietf.org/rfc/rfc3369.txt
+-- http://www.alvestrand.no/objectid
+-- http://www.itu.int/ITU-T/asn1/database
+-- BUT BE CAREFUL !!!
+
+Any ::= ANY -- do not remove!
+
+ContentInfo ::= SEQUENCE {
+     contentType OBJECT IDENTIFIER,
+     content [0] EXPLICIT ANY }
+
+EnvelopedData ::= SEQUENCE {
+         version ANY,
+         originatorInfo [0] ANY OPTIONAL,
+         recipientInfos RecipientInfos,
+         encryptedContentInfo ANY,
+         unprotectedAttrs [1] ANY OPTIONAL
+}
+RecipientInfos ::= SET OF RecipientInfo
+RecipientInfo ::= CHOICE {
+  keyAgreementRecipientInfo      [1] SEQUENCE OF KeyAgreementRecipientInfo,
+  keyTransportRecipientInfo      ANY
+}
+KeyAgreementRecipientInfo ::= SEQUENCE {
+  version                 ANY,
+  originator              ANY,
+  userKeyingMaterial      [1] ANY OPTIONAL,
+  keyEncryptionAlgorithm  ANY,
+  recipientEncryptedKeys  SEQUENCE OF RecipientEncryptedKey
+}
+RecipientEncryptedKey ::= SEQUENCE {
+  recipientIdentifier  SomebodyIdentifier,
+  encryptedKey         ANY
+}
+SomebodyIdentifier ::= CHOICE {
+  issuerAndSerialNumber  IssuerAndSerialNumber,
+  recipientKeyIdentifier [0] ANY,
+  subjectKeyIdentifier   [2] ANY
+}
+IssuerAndSerialNumber ::= SEQUENCE {
+  issuer        ANY,
+  serialNumber  INTEGER
+}
+
+SignerIdentifier ::= CHOICE {
+     issuerAndSerialNumber IssuerAndSerialNumber,
+     subjectKeyIdentifier [0] ANY
+}
+
+SignedAndEnvelopedData ::= SEQUENCE {
+  version               ANY,
+  recipientInfos        RecipientInfos,
+  digestAlgorithms      ANY,
+  encryptedContentInfo  ANY,
+  certificates          [0] ANY OPTIONAL,
+  crls                  [1] ANY OPTIONAL,
+  signerInfos           SET OF SignerInfo }
+
+EncapsulatedContentInfo ::= SEQUENCE {
+     eContentType OBJECT IDENTIFIER,
+     eContent [0] EXPLICIT OCTET STRING OPTIONAL }
+
+
+SignedData ::= SEQUENCE {
+     version INTEGER,
+     digestAlgorithms ANY,
+     contentInfo EncapsulatedContentInfo,
+     certificates [0] ANY OPTIONAL,
+     crls [1] ANY OPTIONAL,
+     test1 ANY,
+     test ANY
+}
+SignerInfo ::= SEQUENCE {
+     version INTEGER,
+     signerIdentifier SignerIdentifier, 
+     issuerAndSerialNumber IssuerAndSerialNumber,
+     digestAlgorithm ANY,
+     authenticatedAttributes [0] ANY OPTIONAL,
+     digestEncryptionAlgorithm ANY,
+     encryptedDigest ANY,
+     unauthenticatedAttributes [1] ANY OPTIONAL }
+
+DigestedData ::= SEQUENCE {
+  version          ANY,
+  digestAlgorithm  ANY,
+  contentInfo      ContentInfo,
+  digest           ANY }
+EncryptedData ::= SEQUENCE {
+  version                ANY,
+  encryptedContentInfo   EncryptedContentInfo,
+  unprotectedAttributes  [1] ANY OPTIONAL }
+EncryptedContentInfo ::= SEQUENCE {
+  contentType          OBJECT IDENTIFIER,
+  contentEncAlgorithm  ANY,
+  encryptedContent     [0] ANY OPTIONAL }
+Data ::= OCTET STRING
+
+ASN1
+
+      my $node = $asn1->find('ContentInfo');
+      my @d = $node->decode($drmmetadata);
+      my %oidmap = (
+	  '1.2.840.113549.1.7.1' => 'Data',
+	  '1.2.840.113549.1.7.2' => 'SignedData',
+	  '1.2.840.113549.1.7.3' => 'EnvelopedData',
+	  '1.2.840.113549.1.7.4' => 'SignedAndEnvelopedData',
+	  '1.2.840.113549.1.7.5' => 'DigestedData',
+	  '1.2.840.113549.1.7.6' => 'EncryptedData',
+      );
+      info Dumper($d[1]);
+      my $type = $oidmap{$d[0]->{contentType}};
+      my $data = $d[0]->{content};
+      info "Content Type is $type";
+      $node = $asn1->find($type);
+      @d = $node->decode($data);
+      $type = $oidmap{$d[0]->{contentInfo}->{eContentType}};
+      info "Got eContentData of type $type";
+      $data = $d[0]->{contentInfo}->{eContent};
+      open (my $fh, '>/tmp/tmp.asn1') || die;
+      print $fh $data;
+      close($fh);
+      $node = $asn1->find($type);
+      @d = $node->decode($data);
+      info Dumper(@d);
+      
+  }
 
   my $bb = read_bootstrap_box(decode_base64($media->{bootstrapInfo}->{content}));
-
   debug 'Parsed media bootstrap data is '.Dumper($bb);
   debug 'Parsed media metadata is '.Dumper($media);
 
@@ -405,8 +694,11 @@
 	      $s = substr($s, $a->{len});
 	  }
 	  die "Didn't find the movie data box in fragment $frag" if (!defined($dat));
+	  if (defined($encryptionkey)) {
+	      $dat = flv_decrypt($dat, $encryptionkey);
+	  }
 	  $filebytecount += length($dat);
-	  print STDERR sprintf("%s: %d (Frag %d: %.2f\%)\r", $file, $filebytecount, $frag, ($filebytecount/$media->{md}->{filesize})*100);
+	  print STDERR sprintf("%s: %d (Frag %d: %.2f\%)\r", $file, $filebytecount, $frag, $frag/$totfrags*100);
 	  print $fh $dat;
       }
       close($fh);