|
1 | 1 | package widevine |
2 | 2 |
|
3 | | -// copied from https://github.com/Eyevinn/mp4ff/blob/master/examples/decrypt-cenc/main.go |
4 | 3 | import ( |
5 | | - "bytes" |
| 4 | + "errors" |
6 | 5 | "fmt" |
7 | 6 | "io" |
8 | 7 |
|
9 | 8 | "github.com/Eyevinn/mp4ff/mp4" |
10 | | -) |
11 | 9 |
|
12 | | -const ( |
13 | | - schemeCENC = "cenc" |
14 | | - schemeCBCS = "cbcs" |
| 10 | + wvpb "github.com/iyear/gowidevine/widevinepb" |
15 | 11 | ) |
16 | 12 |
|
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 |
75 | 24 | } |
76 | 25 | } |
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) |
100 | 28 | } |
| 29 | + // Execute decryption |
| 30 | + return DecryptMP4(r, key, w) |
| 31 | +} |
101 | 32 |
|
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) |
103 | 37 | if err != nil { |
104 | | - return err |
| 38 | + return fmt.Errorf("failed to decode file: %w", err) |
105 | 39 | } |
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") |
120 | 42 | } |
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") |
143 | 46 | } |
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) |
184 | 50 | } |
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) |
191 | 53 | } |
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) |
212 | 64 | } |
213 | | - } else if tenc.DefaultConstantIV != nil { |
214 | | - iv = tenc.DefaultConstantIV |
215 | 65 | } |
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) |
235 | 68 | } |
236 | 69 | } |
237 | 70 | return nil |
|
0 commit comments