Passed
Pull Request — master (#262)
by Pascal
03:15
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
    const HLS_KEY_INFO_FILENAME = 'hls_encryption.keyinfo';
13
14
    /**
15
     * The encryption key.
16
     *
17
     * @var string
18
     */
19
    private $encryptionKey;
20
21
    /**
22
     * Gets called whenever a new encryption key is set.
23
     *
24
     * @var callable
25
     */
26
    private $onNewEncryptionKey ;
27
28
    /**
29
     * Disk to store the secrets.
30
     *
31
     * @var \ProtoneMedia\LaravelFFMpeg\Filesystem\Disk
32
     */
33
    private $encryptionSecretsDisk;
34
35
    /**
36
     * Encryption IV
37
     *
38
     * @var string
39
     */
40
    private $encryptionIV;
41
42
    /**
43
     * Wether to rotate the key on every segment.
44
     *
45
     * @var boolean
46
     */
47
    private $rotatingEncryptiongKey = false;
48
49
    /**
50
     * Creates a new encryption key.
51
     *
52
     * @return string
53
     */
54
    public static function generateEncryptionKey(): string
55
    {
56
        return random_bytes(16);
57
    }
58
59
    /**
60
     * Sets the encryption key with the given value or generates a new one.
61
     *
62
     * @param string $key
63
     * @return string
64
     */
65
    private function setEncryptionKey($key = null): string
66
    {
67
        return $this->encryptionKey = $key ?: static::generateEncryptionKey();
68
    }
69
70
    /**
71
     * Initialises the disk, info and IV for encryption and sets the key.
72
     *
73
     * @param string $key
74
     * @return self
75
     */
76
    public function withEncryptionKey($key = null): self
77
    {
78
        $this->encryptionSecretsDisk = Disk::makeTemporaryDisk();
79
        $this->encryptionIV          = bin2hex(static::generateEncryptionKey());
80
81
        $this->setEncryptionKey($key);
82
83
        return $this;
84
    }
85
86
    /**
87
     * Enables encryption with rotating keys.
88
     *
89
     * @return self
90
     */
91
    public function withRotatingEncryptionKey(): self
92
    {
93
        $this->rotatingEncryptiongKey = true;
94
95
        return $this->withEncryptionKey();
96
    }
97
98
    /**
99
     * A callable for each key that is generated.
100
     *
101
     * @param Closure $callback
102
     * @return self
103
     */
104
    public function onNewEncryptionKey(Closure $callback): self
105
    {
106
        $this->onNewEncryptionKey = $callback;
107
108
        return $this;
109
    }
110
111
    /**
112
     * Rotates the key and returns the absolute path to the info file.
113
     *
114
     * @return string
115
     */
116
    private function rotateEncryptionKey(): string
117
    {
118
        // randomize the encryption key
119
        $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

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

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

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

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