Skip to content

Commit

Permalink
Adapt to mp4ff v0.46.0 (#26)
Browse files Browse the repository at this point in the history
* upgrade to mp4ff lib v0.46.0 and adapt DecryptMP4 to it

* gci sort imports

* cleanup

* avoid breaking change, add a wrapper

* readd support of partialy encrypted samples
  • Loading branch information
hekmon authored Oct 2, 2024
1 parent 4f85b53 commit 2ba3ad2
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 219 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
257 changes: 45 additions & 212 deletions decrypt.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down

0 comments on commit 2ba3ad2

Please sign in to comment.