Skip to content

Commit 2ba3ad2

Browse files
authored
Adapt to mp4ff v0.46.0 (#26)
* 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
1 parent 4f85b53 commit 2ba3ad2

File tree

4 files changed

+52
-219
lines changed

4 files changed

+52
-219
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ func main() {
7272
fmt.Printf("type: %s, id: %x, key: %x\n", key.Type, key.ID, key.Key)
7373
}
7474

75-
err = widevine.DecryptMP4(bytes.NewBufferString("encrypted data"),
76-
keys[0].Key, io.Discard)
75+
err = widevine.DecryptMP4Auto(bytes.NewBufferString("encrypted data"),
76+
keys, io.Discard)
7777
if err != nil {
7878
panic(err)
7979
}

decrypt.go

Lines changed: 45 additions & 212 deletions
Original file line numberDiff line numberDiff line change
@@ -1,237 +1,70 @@
11
package widevine
22

3-
// copied from https://github.com/Eyevinn/mp4ff/blob/master/examples/decrypt-cenc/main.go
43
import (
5-
"bytes"
4+
"errors"
65
"fmt"
76
"io"
87

98
"github.com/Eyevinn/mp4ff/mp4"
10-
)
119

12-
const (
13-
schemeCENC = "cenc"
14-
schemeCBCS = "cbcs"
10+
wvpb "github.com/iyear/gowidevine/widevinepb"
1511
)
1612

17-
// DecryptMP4 decrypts a fragmented MP4 file with the given key. Supports CENC and CBCS schemes.
18-
func DecryptMP4(r io.Reader, key []byte, w io.Writer) error {
19-
inMp4, err := mp4.DecodeFile(r)
20-
if err != nil {
21-
return err
22-
}
23-
if !inMp4.IsFragmented() {
24-
return fmt.Errorf("file not fragmented. Not supported")
25-
}
26-
27-
tracks := make([]trackInfo, 0, len(inMp4.Init.Moov.Traks))
28-
29-
moov := inMp4.Init.Moov
30-
31-
for _, trak := range moov.Traks {
32-
trackID := trak.Tkhd.TrackID
33-
stsd := trak.Mdia.Minf.Stbl.Stsd
34-
var encv *mp4.VisualSampleEntryBox
35-
var enca *mp4.AudioSampleEntryBox
36-
var schemeType string
37-
38-
for _, child := range stsd.Children {
39-
switch child.Type() {
40-
case "encv":
41-
encv = child.(*mp4.VisualSampleEntryBox)
42-
sinf, err := encv.RemoveEncryption()
43-
if err != nil {
44-
return err
45-
}
46-
schemeType = sinf.Schm.SchemeType
47-
tracks = append(tracks, trackInfo{
48-
trackID: trackID,
49-
sinf: sinf,
50-
})
51-
case "enca":
52-
enca = child.(*mp4.AudioSampleEntryBox)
53-
sinf, err := enca.RemoveEncryption()
54-
if err != nil {
55-
return err
56-
}
57-
schemeType = sinf.Schm.SchemeType
58-
tracks = append(tracks, trackInfo{
59-
trackID: trackID,
60-
sinf: sinf,
61-
})
62-
default:
63-
continue
64-
}
65-
}
66-
if schemeType != "" && schemeType != schemeCENC && schemeType != schemeCBCS {
67-
return fmt.Errorf("scheme type %s not supported", schemeType)
68-
}
69-
if schemeType == "" {
70-
// Should be track in the clear
71-
tracks = append(tracks, trackInfo{
72-
trackID: trackID,
73-
sinf: nil,
74-
})
13+
// Adapted from https://github.com/Eyevinn/mp4ff/blob/v0.46.0/cmd/mp4ff-decrypt/main.go
14+
15+
// DecryptMP4Auto decrypts a fragmented MP4 file with the set of keys retreived from the widevice license
16+
// by automatically selecting the appropriate key. Supports CENC and CBCS schemes.
17+
func DecryptMP4Auto(r io.Reader, keys []*Key, w io.Writer) error {
18+
// Extract content key
19+
var key []byte
20+
for _, k := range keys {
21+
if k.Type == wvpb.License_KeyContainer_CONTENT {
22+
key = k.Key
23+
break
7524
}
7625
}
77-
78-
for _, trex := range moov.Mvex.Trexs {
79-
for i := range tracks {
80-
if tracks[i].trackID == trex.TrackID {
81-
tracks[i].trex = trex
82-
break
83-
}
84-
}
85-
}
86-
psshs := moov.RemovePsshs()
87-
for _, pssh := range psshs {
88-
psshInfo := bytes.Buffer{}
89-
err = pssh.Info(&psshInfo, "", "", " ")
90-
if err != nil {
91-
return err
92-
}
93-
// fmt.Printf("pssh: %s\n", psshInfo.String())
94-
}
95-
96-
// Write the modified init segment
97-
err = inMp4.Init.Encode(w)
98-
if err != nil {
99-
return err
26+
if key == nil {
27+
return fmt.Errorf("no %s key type found in the provided key set", wvpb.License_KeyContainer_CONTENT)
10028
}
29+
// Execute decryption
30+
return DecryptMP4(r, key, w)
31+
}
10132

102-
err = decryptAndWriteSegments(inMp4.Segments, tracks, key, w)
33+
// DecryptMP4 decrypts a fragmented MP4 file with keys from widevice license. Supports CENC and CBCS schemes.
34+
func DecryptMP4(r io.Reader, key []byte, w io.Writer) error {
35+
// Initialization
36+
inMp4, err := mp4.DecodeFile(r)
10337
if err != nil {
104-
return err
38+
return fmt.Errorf("failed to decode file: %w", err)
10539
}
106-
return nil
107-
}
108-
109-
type trackInfo struct {
110-
trackID uint32
111-
sinf *mp4.SinfBox
112-
trex *mp4.TrexBox
113-
}
114-
115-
func findTrackInfo(tracks []trackInfo, trackID uint32) trackInfo {
116-
for _, ti := range tracks {
117-
if ti.trackID == trackID {
118-
return ti
119-
}
40+
if !inMp4.IsFragmented() {
41+
return errors.New("file is not fragmented")
12042
}
121-
return trackInfo{}
122-
}
123-
124-
func decryptAndWriteSegments(segs []*mp4.MediaSegment, tracks []trackInfo, key []byte, ofh io.Writer) error {
125-
var outNr uint32 = 1
126-
for _, seg := range segs {
127-
for _, frag := range seg.Fragments {
128-
// fmt.Printf("Segment %d, fragment %d\n", i+1, j+1)
129-
err := decryptFragment(frag, tracks, key)
130-
if err != nil {
131-
return err
132-
}
133-
outNr++
134-
}
135-
if len(seg.Sidxs) > 0 {
136-
seg.Sidx = nil // drop sidx inside segment, since not modified properly
137-
seg.Sidxs = nil
138-
}
139-
err := seg.Encode(ofh)
140-
if err != nil {
141-
return err
142-
}
43+
// Handle init segment
44+
if inMp4.Init == nil {
45+
return errors.New("no init part of file")
14346
}
144-
145-
return nil
146-
}
147-
148-
// decryptFragment - decrypt fragment in place
149-
func decryptFragment(frag *mp4.Fragment, tracks []trackInfo, key []byte) error {
150-
moof := frag.Moof
151-
var nrBytesRemoved uint64 = 0
152-
for _, traf := range moof.Trafs {
153-
ti := findTrackInfo(tracks, traf.Tfhd.TrackID)
154-
if ti.sinf != nil {
155-
schemeType := ti.sinf.Schm.SchemeType
156-
if schemeType != schemeCENC && schemeType != schemeCBCS {
157-
return fmt.Errorf("scheme type %s not supported", schemeType)
158-
}
159-
hasSenc, isParsed := traf.ContainsSencBox()
160-
if !hasSenc {
161-
// if not encrypted, do nothing
162-
continue
163-
}
164-
if !isParsed {
165-
defaultPerSampleIVSize := ti.sinf.Schi.Tenc.DefaultPerSampleIVSize
166-
err := traf.ParseReadSenc(defaultPerSampleIVSize, moof.StartPos)
167-
if err != nil {
168-
return fmt.Errorf("parseReadSenc: %w", err)
169-
}
170-
}
171-
172-
tenc := ti.sinf.Schi.Tenc
173-
samples, err := frag.GetFullSamples(ti.trex)
174-
if err != nil {
175-
return err
176-
}
177-
178-
err = decryptSamplesInPlace(schemeType, samples, key, tenc, traf.Senc)
179-
if err != nil {
180-
return err
181-
}
182-
nrBytesRemoved += traf.RemoveEncryptionBoxes()
183-
}
47+
decryptInfo, err := mp4.DecryptInit(inMp4.Init)
48+
if err != nil {
49+
return fmt.Errorf("failed to decrypt init: %w", err)
18450
}
185-
_, psshBytesRemoved := moof.RemovePsshs()
186-
nrBytesRemoved += psshBytesRemoved
187-
for _, traf := range moof.Trafs {
188-
for _, trun := range traf.Truns {
189-
trun.DataOffset -= int32(nrBytesRemoved)
190-
}
51+
if err = inMp4.Init.Encode(w); err != nil {
52+
return fmt.Errorf("failed to write init: %w", err)
19153
}
192-
193-
return nil
194-
}
195-
196-
// decryptSample - decrypt samples inplace
197-
func decryptSamplesInPlace(schemeType string, samples []mp4.FullSample, key []byte, tenc *mp4.TencBox, senc *mp4.SencBox) error {
198-
// TODO. Interpret saio and saiz to get to the right place
199-
// Saio tells where the IV starts relative to moof start
200-
// It typically ends up inside senc (16 bytes after start)
201-
202-
for i := range samples {
203-
encSample := samples[i].Data
204-
var iv []byte
205-
if len(senc.IVs) == len(samples) {
206-
if len(senc.IVs[i]) == 8 {
207-
iv = make([]byte, 0, 16)
208-
iv = append(iv, senc.IVs[i]...)
209-
iv = append(iv, []byte{0, 0, 0, 0, 0, 0, 0, 0}...)
210-
} else if len(senc.IVs) == len(samples) {
211-
iv = senc.IVs[i]
54+
// Decode segments
55+
for _, seg := range inMp4.Segments {
56+
if err = mp4.DecryptSegment(seg, decryptInfo, key); err != nil {
57+
if err.Error() == "no senc box in traf" {
58+
// No SENC box, skip decryption for this segment as samples can have
59+
// unencrypted segments followed by encrypted segments. See:
60+
// https://github.com/iyear/gowidevine/pull/26#issuecomment-2385960551
61+
err = nil
62+
} else {
63+
return fmt.Errorf("failed to decrypt segment: %w", err)
21264
}
213-
} else if tenc.DefaultConstantIV != nil {
214-
iv = tenc.DefaultConstantIV
21565
}
216-
if len(iv) == 0 {
217-
return fmt.Errorf("iv has length 0")
218-
}
219-
220-
var subSamplePatterns []mp4.SubSamplePattern
221-
if len(senc.SubSamples) != 0 {
222-
subSamplePatterns = senc.SubSamples[i]
223-
}
224-
switch schemeType {
225-
case schemeCENC:
226-
err := mp4.DecryptSampleCenc(encSample, key, iv, subSamplePatterns)
227-
if err != nil {
228-
return err
229-
}
230-
case schemeCBCS:
231-
err := mp4.DecryptSampleCbcs(encSample, key, iv, subSamplePatterns, tenc)
232-
if err != nil {
233-
return err
234-
}
66+
if err = seg.Encode(w); err != nil {
67+
return fmt.Errorf("failed to encode segment: %w", err)
23568
}
23669
}
23770
return nil

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/iyear/gowidevine
33
go 1.18
44

55
require (
6-
github.com/Eyevinn/mp4ff v0.39.0
6+
github.com/Eyevinn/mp4ff v0.46.0
77
github.com/chmike/cmac-go v1.1.0
88
github.com/stretchr/testify v1.9.0
99
google.golang.org/protobuf v1.34.2

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
github.com/Eyevinn/mp4ff v0.39.0 h1:WV2Omq57y1BvcVPayjyuiIK8/pF5Tb/H/cgPY+wFZMQ=
2-
github.com/Eyevinn/mp4ff v0.39.0/go.mod h1:w/6GSa5ghZ1VavzJK6McQ2/flx8mKtcrKDr11SsEweA=
1+
github.com/Eyevinn/mp4ff v0.46.0 h1:A8oJA4A3C9fDbX38jEw/26utjNdvmRmrO37tVI5pDk0=
2+
github.com/Eyevinn/mp4ff v0.46.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg=
33
github.com/chmike/cmac-go v1.1.0 h1:aF73ZAEx9N2WdQc93DOJ2fMsBDAGqUtuenjMJMb3kEI=
44
github.com/chmike/cmac-go v1.1.0/go.mod h1:wcIN7NRqWSKGuORzd4dReBkoBDE9ZBqfyTVxyDxGeUw=
55
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
66
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7-
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
8-
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
7+
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
8+
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
99
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
1010
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1111
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

0 commit comments

Comments
 (0)