diff options
-rw-r--r-- | lib/FlashVideo/F4VDownloader.pm | 300 |
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); |