Passed
Pull Request — master (#262)
by Pascal
04:38 queued 01:30
created

EncryptsHLSSegments::withRotatingEncryptionKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

117
        $this->encryptionSecretsDisk->/** @scrutinizer ignore-call */ 
118
                                      put(
Loading history...
118
            $keyFilename = Str::random(8) . '.key',
119
            $encryptionKey = $this->setEncryptionKey()
120
        );
121
122
        // get the absolute path to the encryption key
123
        $keyPath = $this->encryptionSecretsDisk->makeMedia($keyFilename)->getLocalPath();
124
125
        // generate an info file with a reference to the encryption key and IV
126
        $this->encryptionSecretsDisk->put(
127
            HLSExporter::HLS_KEY_INFO_FILENAME,
128
            implode(PHP_EOL, [
129
                $keyPath, $keyPath, $this->encryptionIV,
130
            ])
131
        );
132
133
        // call the callback
134
        if ($this->onNewEncryptionKey) {
135
            call_user_func($this->onNewEncryptionKey, $keyFilename, $encryptionKey);
136
        }
137
138
        // return the absolute path to the info file
139
        return $this->encryptionSecretsDisk
140
            ->makeMedia(HLSExporter::HLS_KEY_INFO_FILENAME)
141
            ->getLocalPath();
142
    }
143
144
    /**
145
     * Returns an array with the encryption parameters.
146
     *
147
     * @return array
148
     */
149
    private function getEncrypedHLSParameters(): array
150
    {
151
        if (!$this->encryptionKey) {
152
            return [];
153
        }
154
155
        $keyInfoPath = $this->rotateEncryptionKey();
156
        $parameters  = ['-hls_key_info_file', $keyInfoPath];
157
158
        if ($this->rotateEncryptiongKey) {
159
            $parameters[] = '-hls_flags';
160
            $parameters[] = 'periodic_rekey';
161
        }
162
163
        return $parameters;
164
    }
165
166
    /**
167
     * Adds a listener and handler to rotate the key on
168
     * every new HLS segment.
169
     *
170
     * @return void
171
     */
172
    private function addHandlerToRotateEncryptionKey()
173
    {
174
        if (!$this->rotateEncryptiongKey) {
175
            return;
176
        }
177
178
        $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

178
        $this->/** @scrutinizer ignore-call */ 
179
               addListener(new StdListener)->onEvent('listen', function ($line) {
Loading history...
179
            $opensEncryptedSegment = Str::contains($line, "Opening 'crypto:/")
180
                && Str::contains($line, ".ts' for writing");
181
182
            if ($opensEncryptedSegment) {
183
                $this->rotateEncryptionKey();
184
            }
185
        });
186
    }
187
188
    /**
189
     * Removes the encryption keys from the temporary disk.
190
     *
191
     * @return void
192
     */
193
    private function cleanupHLSEncryption()
194
    {
195
        if (!$this->encryptionSecretsDisk) {
196
            return;
197
        }
198
199
        $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

199
        /** @scrutinizer ignore-call */ 
200
        $paths = $this->encryptionSecretsDisk->allFiles();
Loading history...
200
201
        foreach ($paths as $path) {
202
            $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

202
            $this->encryptionSecretsDisk->/** @scrutinizer ignore-call */ 
203
                                          delete($path);
Loading history...
203
        }
204
    }
205
}
206