aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Bottomley <JBottomley@Parallels.com>2013-07-03 17:30:02 -0700
committerJames Bottomley <JBottomley@Parallels.com>2013-07-03 17:33:27 -0700
commit788ade8a7485d37f535a1c280f150f1d6e83014f (patch)
tree0e6b302abb55c5f26d17511344b5a474afd9d580
parentc740c47627fd22b12dc25a6e556414b8d54d3fe5 (diff)
downloadget-flash-videos-788ade8a7485d37f535a1c280f150f1d6e83014f.tar.gz
F4VDownloader: Add support for parsing DRM header and decrypting videoHEADmaster
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>
-rw-r--r--lib/FlashVideo/F4VDownloader.pm300
1 files changed, 296 insertions, 4 deletions
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 FlashVideo::Utils;
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 @@ sub download {
$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 @@ sub download {
# 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 @@ sub download {
}
}
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
+}
- my $bb = read_bootstrap_box(decode_base64($media->{bootstrapInfo}->{content}));
+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 @@ sub download {
$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);