diff --git a/README.md b/README.md index cfd006b..a67f167 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ func main() { fmt.Printf("type: %s, id: %x, key: %x\n", key.Type, key.ID, key.Key) } - err = widevine.DecryptMP4(bytes.NewBufferString("encrypted data"), - keys[0].Key, io.Discard) + err = widevine.DecryptMP4Auto(bytes.NewBufferString("encrypted data"), + keys, io.Discard) if err != nil { panic(err) } diff --git a/decrypt.go b/decrypt.go index 40af30f..c0bf9ca 100644 --- a/decrypt.go +++ b/decrypt.go @@ -1,237 +1,70 @@ package widevine -// copied from https://github.com/Eyevinn/mp4ff/blob/master/examples/decrypt-cenc/main.go import ( - "bytes" + "errors" "fmt" "io" "github.com/Eyevinn/mp4ff/mp4" -) -const ( - schemeCENC = "cenc" - schemeCBCS = "cbcs" + wvpb "github.com/iyear/gowidevine/widevinepb" ) -// DecryptMP4 decrypts a fragmented MP4 file with the given key. Supports CENC and CBCS schemes. -func DecryptMP4(r io.Reader, key []byte, w io.Writer) error { - inMp4, err := mp4.DecodeFile(r) - if err != nil { - return err - } - if !inMp4.IsFragmented() { - return fmt.Errorf("file not fragmented. Not supported") - } - - tracks := make([]trackInfo, 0, len(inMp4.Init.Moov.Traks)) - - moov := inMp4.Init.Moov - - for _, trak := range moov.Traks { - trackID := trak.Tkhd.TrackID - stsd := trak.Mdia.Minf.Stbl.Stsd - var encv *mp4.VisualSampleEntryBox - var enca *mp4.AudioSampleEntryBox - var schemeType string - - for _, child := range stsd.Children { - switch child.Type() { - case "encv": - encv = child.(*mp4.VisualSampleEntryBox) - sinf, err := encv.RemoveEncryption() - if err != nil { - return err - } - schemeType = sinf.Schm.SchemeType - tracks = append(tracks, trackInfo{ - trackID: trackID, - sinf: sinf, - }) - case "enca": - enca = child.(*mp4.AudioSampleEntryBox) - sinf, err := enca.RemoveEncryption() - if err != nil { - return err - } - schemeType = sinf.Schm.SchemeType - tracks = append(tracks, trackInfo{ - trackID: trackID, - sinf: sinf, - }) - default: - continue - } - } - if schemeType != "" && schemeType != schemeCENC && schemeType != schemeCBCS { - return fmt.Errorf("scheme type %s not supported", schemeType) - } - if schemeType == "" { - // Should be track in the clear - tracks = append(tracks, trackInfo{ - trackID: trackID, - sinf: nil, - }) +// Adapted from https://github.com/Eyevinn/mp4ff/blob/v0.46.0/cmd/mp4ff-decrypt/main.go + +// DecryptMP4Auto decrypts a fragmented MP4 file with the set of keys retreived from the widevice license +// by automatically selecting the appropriate key. Supports CENC and CBCS schemes. +func DecryptMP4Auto(r io.Reader, keys []*Key, w io.Writer) error { + // Extract content key + var key []byte + for _, k := range keys { + if k.Type == wvpb.License_KeyContainer_CONTENT { + key = k.Key + break } } - - for _, trex := range moov.Mvex.Trexs { - for i := range tracks { - if tracks[i].trackID == trex.TrackID { - tracks[i].trex = trex - break - } - } - } - psshs := moov.RemovePsshs() - for _, pssh := range psshs { - psshInfo := bytes.Buffer{} - err = pssh.Info(&psshInfo, "", "", " ") - if err != nil { - return err - } - // fmt.Printf("pssh: %s\n", psshInfo.String()) - } - - // Write the modified init segment - err = inMp4.Init.Encode(w) - if err != nil { - return err + if key == nil { + return fmt.Errorf("no %s key type found in the provided key set", wvpb.License_KeyContainer_CONTENT) } + // Execute decryption + return DecryptMP4(r, key, w) +} - err = decryptAndWriteSegments(inMp4.Segments, tracks, key, w) +// DecryptMP4 decrypts a fragmented MP4 file with keys from widevice license. Supports CENC and CBCS schemes. +func DecryptMP4(r io.Reader, key []byte, w io.Writer) error { + // Initialization + inMp4, err := mp4.DecodeFile(r) if err != nil { - return err + return fmt.Errorf("failed to decode file: %w", err) } - return nil -} - -type trackInfo struct { - trackID uint32 - sinf *mp4.SinfBox - trex *mp4.TrexBox -} - -func findTrackInfo(tracks []trackInfo, trackID uint32) trackInfo { - for _, ti := range tracks { - if ti.trackID == trackID { - return ti - } + if !inMp4.IsFragmented() { + return errors.New("file is not fragmented") } - return trackInfo{} -} - -func decryptAndWriteSegments(segs []*mp4.MediaSegment, tracks []trackInfo, key []byte, ofh io.Writer) error { - var outNr uint32 = 1 - for _, seg := range segs { - for _, frag := range seg.Fragments { - // fmt.Printf("Segment %d, fragment %d\n", i+1, j+1) - err := decryptFragment(frag, tracks, key) - if err != nil { - return err - } - outNr++ - } - if len(seg.Sidxs) > 0 { - seg.Sidx = nil // drop sidx inside segment, since not modified properly - seg.Sidxs = nil - } - err := seg.Encode(ofh) - if err != nil { - return err - } + // Handle init segment + if inMp4.Init == nil { + return errors.New("no init part of file") } - - return nil -} - -// decryptFragment - decrypt fragment in place -func decryptFragment(frag *mp4.Fragment, tracks []trackInfo, key []byte) error { - moof := frag.Moof - var nrBytesRemoved uint64 = 0 - for _, traf := range moof.Trafs { - ti := findTrackInfo(tracks, traf.Tfhd.TrackID) - if ti.sinf != nil { - schemeType := ti.sinf.Schm.SchemeType - if schemeType != schemeCENC && schemeType != schemeCBCS { - return fmt.Errorf("scheme type %s not supported", schemeType) - } - hasSenc, isParsed := traf.ContainsSencBox() - if !hasSenc { - // if not encrypted, do nothing - continue - } - if !isParsed { - defaultPerSampleIVSize := ti.sinf.Schi.Tenc.DefaultPerSampleIVSize - err := traf.ParseReadSenc(defaultPerSampleIVSize, moof.StartPos) - if err != nil { - return fmt.Errorf("parseReadSenc: %w", err) - } - } - - tenc := ti.sinf.Schi.Tenc - samples, err := frag.GetFullSamples(ti.trex) - if err != nil { - return err - } - - err = decryptSamplesInPlace(schemeType, samples, key, tenc, traf.Senc) - if err != nil { - return err - } - nrBytesRemoved += traf.RemoveEncryptionBoxes() - } + decryptInfo, err := mp4.DecryptInit(inMp4.Init) + if err != nil { + return fmt.Errorf("failed to decrypt init: %w", err) } - _, psshBytesRemoved := moof.RemovePsshs() - nrBytesRemoved += psshBytesRemoved - for _, traf := range moof.Trafs { - for _, trun := range traf.Truns { - trun.DataOffset -= int32(nrBytesRemoved) - } + if err = inMp4.Init.Encode(w); err != nil { + return fmt.Errorf("failed to write init: %w", err) } - - return nil -} - -// decryptSample - decrypt samples inplace -func decryptSamplesInPlace(schemeType string, samples []mp4.FullSample, key []byte, tenc *mp4.TencBox, senc *mp4.SencBox) error { - // TODO. Interpret saio and saiz to get to the right place - // Saio tells where the IV starts relative to moof start - // It typically ends up inside senc (16 bytes after start) - - for i := range samples { - encSample := samples[i].Data - var iv []byte - if len(senc.IVs) == len(samples) { - if len(senc.IVs[i]) == 8 { - iv = make([]byte, 0, 16) - iv = append(iv, senc.IVs[i]...) - iv = append(iv, []byte{0, 0, 0, 0, 0, 0, 0, 0}...) - } else if len(senc.IVs) == len(samples) { - iv = senc.IVs[i] + // Decode segments + for _, seg := range inMp4.Segments { + if err = mp4.DecryptSegment(seg, decryptInfo, key); err != nil { + if err.Error() == "no senc box in traf" { + // No SENC box, skip decryption for this segment as samples can have + // unencrypted segments followed by encrypted segments. See: + // https://github.com/iyear/gowidevine/pull/26#issuecomment-2385960551 + err = nil + } else { + return fmt.Errorf("failed to decrypt segment: %w", err) } - } else if tenc.DefaultConstantIV != nil { - iv = tenc.DefaultConstantIV } - if len(iv) == 0 { - return fmt.Errorf("iv has length 0") - } - - var subSamplePatterns []mp4.SubSamplePattern - if len(senc.SubSamples) != 0 { - subSamplePatterns = senc.SubSamples[i] - } - switch schemeType { - case schemeCENC: - err := mp4.DecryptSampleCenc(encSample, key, iv, subSamplePatterns) - if err != nil { - return err - } - case schemeCBCS: - err := mp4.DecryptSampleCbcs(encSample, key, iv, subSamplePatterns, tenc) - if err != nil { - return err - } + if err = seg.Encode(w); err != nil { + return fmt.Errorf("failed to encode segment: %w", err) } } return nil diff --git a/go.mod b/go.mod index a48c432..ee1f987 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/iyear/gowidevine go 1.18 require ( - github.com/Eyevinn/mp4ff v0.39.0 + github.com/Eyevinn/mp4ff v0.46.0 github.com/chmike/cmac-go v1.1.0 github.com/stretchr/testify v1.9.0 google.golang.org/protobuf v1.34.2 diff --git a/go.sum b/go.sum index 0f3ecbf..7c4b4cc 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ -github.com/Eyevinn/mp4ff v0.39.0 h1:WV2Omq57y1BvcVPayjyuiIK8/pF5Tb/H/cgPY+wFZMQ= -github.com/Eyevinn/mp4ff v0.39.0/go.mod h1:w/6GSa5ghZ1VavzJK6McQ2/flx8mKtcrKDr11SsEweA= +github.com/Eyevinn/mp4ff v0.46.0 h1:A8oJA4A3C9fDbX38jEw/26utjNdvmRmrO37tVI5pDk0= +github.com/Eyevinn/mp4ff v0.46.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg= github.com/chmike/cmac-go v1.1.0 h1:aF73ZAEx9N2WdQc93DOJ2fMsBDAGqUtuenjMJMb3kEI= github.com/chmike/cmac-go v1.1.0/go.mod h1:wcIN7NRqWSKGuORzd4dReBkoBDE9ZBqfyTVxyDxGeUw= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=