aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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);