Passed
Pull Request — master (#262)
by Pascal
02:27
created

EncryptsHLSSegments::generateEncryptionKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace ProtoneMedia\LaravelFFMpeg\Exporters;
4
5
use Closure;
6
use Illuminate\Support\Collection;
7
use Illuminate\Support\Str;
8
use ProtoneMedia\LaravelFFMpeg\FFMpeg\StdListener;
9
use ProtoneMedia\LaravelFFMpeg\Filesystem\Disk;
10
11
trait EncryptsHLSSegments
12
{
13
    /**
14
     * The encryption key.
15
     *
16
     * @var string
17
     */
18
    private $encryptionKey;
19
20
    /**
21
     * Gets called whenever a new encryption key is set.
22
     *
23
     * @var callable
24
     */
25
    private $onNewEncryptionKey;
26
27
    /**
28
     * Disk to store the secrets.
29
     *
30
     * @var \ProtoneMedia\LaravelFFMpeg\Filesystem\Disk
31
     */
32
    private $encryptionSecretsDisk;
33
34
    /**
35
     * Encryption IV
36
     *
37
     * @var string
38
     */
39
    private $encryptionIV;
40
41
    /**
42
     * Wether to rotate the key on every segment.
43
     *
44
     * @var boolean
45
     */
46
    private $rotateEncryptiongKey = false;
47
48
    /**
49
     * Creates a new encryption key.
50
     *
51
     * @return string
52
     */
53
    public static function generateEncryptionKey(): string
54
    {
55
        return random_bytes(16);
56
    }
57
58
    /**
59
     * Sets the encryption key with the given value or generates a new one.
60
     *
61
     * @param string $key
62
     * @return string
63
     */
64
    private function setEncryptionKey($key = null): string
65
    {
66
        return $this->encryptionKey = $key ?: static::generateEncryptionKey();
67
    }
68
69
    /**
70
     * Initialises the disk, info and IV for encryption and sets the key.
71
     *
72
     * @param string $key
73
     * @return self
74
     */
75
    public function withEncryptionKey($key = null): self
76
    {
77
        $this->encryptionSecretsDisk = Disk::makeTemporaryDisk();
78
        $this->encryptionIV          = bin2hex(static::generateEncryptionKey());
79
80
        $this->setEncryptionKey($key);
81
82
        return $this;
83
    }
84
85
    /**
86
     * Enables encryption with rotating keys.
87
     *
88
     * @return self
89
     */
90
    public function withRotatingEncryptionKey(): self
91
    {
92
        $this->rotateEncryptiongKey = true;
93
94
        return $this->withEncryptionKey();
95
    }
96
97
    /**
98
     * A callable for each key that is generated.
99
     *
100
     * @param Closure $callback
101
     * @return self
102
     */
103
    public function onNewEncryptionKey(Closure $callback): self
104
    {
105
        $this->onNewEncryptionKey = $callback;
106
107
        return $this;
108
    }
109
110
    /**
111
     * Rotates the key and returns the absolute path to the info file.
112
     *
113
     * @return string
114
     */
115
    private function rotateEncryptionKey(): string
116
    {
117
        // randomize the encryption key
118
        $this->encryptionSecretsDisk->put(
0 ignored issues
show
Bug introduced by
The method put() does not exist on ProtoneMedia\LaravelFFMpeg\Filesystem\Disk. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

118
        $this->encryptionSecretsDisk->/** @scrutinizer ignore-call */ 
119
                                      put(
Loading history...
119
            $keyFilename = uniqid() . '.key',
120
            $encryptionKey = $this->setEncryptionKey()
121
        );
122
123
        // get the absolute path to the encryption key
124
        $keyPath = $this->encryptionSecretsDisk->makeMedia($keyFilename)->getLocalPath();
125
126
        // generate an info file with a reference to the encryption key and IV
127
        $this->encryptionSecretsDisk->put(
128
            HLSExporter::HLS_KEY_INFO_FILENAME,
129
            implode(PHP_EOL, [
130
                $keyPath, $keyPath, $this->encryptionIV,
131
            ])
132
        );
133
134
        // call the callback
135
        if ($this->onNewEncryptionKey) {
136
            call_user_func($this->onNewEncryptionKey, $keyFilename, $encryptionKey);
137
        }
138
139
        // return the absolute path to the info file
140
        return $this->encryptionSecretsDisk
141
            ->makeMedia(HLSExporter::HLS_KEY_INFO_FILENAME)
142
            ->getLocalPath();
143
    }
144
145
    /**
146
     * Returns an array with the encryption parameters.
147
     *
148
     * @return array
149
     */
150
    private function getEncrypedHLSParameters(): array
151
    {
152
        if (!$this->encryptionKey) {
153
            return [];
154
        }
155
156
        $keyInfoPath = $this->rotateEncryptionKey();
157
        $parameters  = ['-hls_key_info_file', $keyInfoPath];
158
159
        if ($this->rotateEncryptiongKey) {
160
            $parameters[] = '-hls_flags';
161
            $parameters[] = 'periodic_rekey';
162
        }
163
164
        return $parameters;
165
    }
166
167
    /**
168
     * Adds a listener and handler to rotate the key on
169
     * every new HLS segment.
170
     *
171
     * @return void
172
     */
173
    private function addHandlerToRotateEncryptionKey()
174
    {
175
        if (!$this->rotateEncryptiongKey) {
176
            return;
177
        }
178
179
        $this->addListener(new StdListener)->onEvent('listen', function ($line) {
0 ignored issues
show
Bug introduced by
It seems like addListener() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

179
        $this->/** @scrutinizer ignore-call */ 
180
               addListener(new StdListener)->onEvent('listen', function ($line) {
Loading history...
180
            $opensEncryptedSegment = Str::contains($line, "Opening 'crypto:/")
181
                && Str::contains($line, ".ts' for writing");
182
183
            if ($opensEncryptedSegment) {
184
                $this->rotateEncryptionKey();
185
            }
186
        });
187
    }
188
189
    private function replaceAbsolutePathsHLSEncryption(Collection $playlistMedia)
190
    {
191
        if (!$this->encryptionSecretsDisk) {
192
            return;
193
        }
194
195
        $playlistMedia->each(function ($playlistMedia) {
196
            $disk = $playlistMedia->getDisk();
197
            $path = $playlistMedia->getPath();
198
199
            $prefix = '#EXT-X-KEY:METHOD=AES-128,URI="';
200
201
            $content = str_replace(
202
                $prefix . $this->encryptionSecretsDisk->path(''),
0 ignored issues
show
Bug introduced by
The method path() does not exist on ProtoneMedia\LaravelFFMpeg\Filesystem\Disk. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

202
                $prefix . $this->encryptionSecretsDisk->/** @scrutinizer ignore-call */ path(''),
Loading history...
203
                $prefix,
204
                $disk->get($path)
205
            );
206
207
            $disk->put($path, $content);
208
        });
209
    }
210
211
    /**
212
     * Removes the encryption keys from the temporary disk.
213
     *
214
     * @return void
215
     */
216
    private function cleanupHLSEncryption()
217
    {
218
        if (!$this->encryptionSecretsDisk) {
219
            return;
220
        }
221
222
        $paths = $this->encryptionSecretsDisk->allFiles();
0 ignored issues
show
Bug introduced by
The method allFiles() does not exist on ProtoneMedia\LaravelFFMpeg\Filesystem\Disk. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

222
        /** @scrutinizer ignore-call */ 
223
        $paths = $this->encryptionSecretsDisk->allFiles();
Loading history...
223
224
        foreach ($paths as $path) {
225
            $this->encryptionSecretsDisk->delete($path);
0 ignored issues
show
Bug introduced by
The method delete() does not exist on ProtoneMedia\LaravelFFMpeg\Filesystem\Disk. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

225
            $this->encryptionSecretsDisk->/** @scrutinizer ignore-call */ 
226
                                          delete($path);
Loading history...
226
        }
227
    }
228
}
229