forked from j62/ctbrec
1
0
Fork 0

Add support for HLS AES encryption

This commit is contained in:
0xboobface 2019-04-12 19:33:18 +02:00
parent 151b6a531d
commit ca2ceb7f43
4 changed files with 106 additions and 21 deletions

View File

@ -24,6 +24,7 @@ import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.EncryptionData;
import com.iheartradio.m3u8.data.MediaPlaylist; import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.TrackData; import com.iheartradio.m3u8.data.TrackData;
@ -87,6 +88,12 @@ public abstract class AbstractHlsDownload implements Download {
lsp.totalDuration += trackData.getTrackInfo().duration; lsp.totalDuration += trackData.getTrackInfo().duration;
lsp.lastSegDuration = trackData.getTrackInfo().duration; lsp.lastSegDuration = trackData.getTrackInfo().duration;
lsp.segments.add(uri); lsp.segments.add(uri);
if(trackData.hasEncryptionData()) {
lsp.encrypted = true;
EncryptionData data = trackData.getEncryptionData();
lsp.encryptionKeyUrl = data.getUri();
lsp.encryptionMethod = data.getMethod().getValue();
}
} }
return lsp; return lsp;
} }
@ -155,6 +162,9 @@ public abstract class AbstractHlsDownload implements Download {
public float lastSegDuration = 0; public float lastSegDuration = 0;
public float targetDuration = 0; public float targetDuration = 0;
public List<String> segments = new ArrayList<>(); public List<String> segments = new ArrayList<>();
public boolean encrypted = false;
public String encryptionMethod = "AES-128";
public String encryptionKeyUrl;
public SegmentPlaylist(String url) { public SegmentPlaylist(String url) {
this.url = url; this.url = url;

View File

@ -0,0 +1,63 @@
package ctbrec.recorder.download;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import okhttp3.Request;
import okhttp3.Response;
public class Crypto {
private byte[] iv = new byte[16];
private byte[] key = new byte[16];
private Cipher cipher;
private HttpClient client;
public Crypto(String encryptionKeyUrl, HttpClient client) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
this.client = client;
loadKey(encryptionKeyUrl);
initCypher();
}
private void initCypher() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKey secretKey = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
}
private void loadKey(String url) throws IOException {
Request request = new Request.Builder().url(url).build();
try (Response response = client.execute(request)) {
if(response.isSuccessful()) {
key = response.body().bytes();
} else {
throw new HttpException(response.code(), response.message());
}
}
}
public byte[] decrypt(byte[] input) throws IllegalBlockSizeException, BadPaddingException {
return cipher.doFinal(input);
}
public CipherInputStream wrap(InputStream in) {
return new CipherInputStream(in, cipher);
}
}

View File

@ -78,28 +78,28 @@ public class HlsDownload extends AbstractHlsDownload {
int nextSegment = 0; int nextSegment = 0;
int waitFactor = 1; int waitFactor = 1;
while(running) { while(running) {
SegmentPlaylist lsp = getNextSegments(segments); SegmentPlaylist playlist = getNextSegments(segments);
if(nextSegment > 0 && lsp.seq > nextSegment) { if(nextSegment > 0 && playlist.seq > nextSegment) {
// TODO switch to a lower bitrate/resolution ?!? // TODO switch to a lower bitrate/resolution ?!?
waitFactor *= 2; waitFactor *= 2;
LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegment, lsp.seq, model, waitFactor); LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegment, playlist.seq, model, waitFactor);
} }
int skip = nextSegment - lsp.seq; int skip = nextSegment - playlist.seq;
for (String segment : lsp.segments) { for (String segment : playlist.segments) {
if(skip > 0) { if(skip > 0) {
skip--; skip--;
} else { } else {
URL segmentUrl = new URL(segment); URL segmentUrl = new URL(segment);
String prefix = nf.format(segmentCounter++); String prefix = nf.format(segmentCounter++);
downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client, prefix)); downloadThreadPool.submit(new SegmentDownload(playlist, segmentUrl, downloadDir, client, prefix));
//new SegmentDownload(segment, downloadDir).call(); //new SegmentDownload(segment, downloadDir).call();
} }
} }
long wait = 0; long wait = 0;
if(lastSegment == lsp.seq) { if(lastSegment == playlist.seq) {
// playlist didn't change -> wait for at least half the target duration // playlist didn't change -> wait for at least half the target duration
wait = (long) lsp.targetDuration * 1000 / waitFactor; wait = (long) playlist.targetDuration * 1000 / waitFactor;
LOG.trace("Playlist didn't change... waiting for {}ms", wait); LOG.trace("Playlist didn't change... waiting for {}ms", wait);
} else { } else {
// playlist did change -> wait for at least last segment duration // playlist did change -> wait for at least last segment duration
@ -117,9 +117,9 @@ public class HlsDownload extends AbstractHlsDownload {
// this if check makes sure, that we don't decrease nextSegment. for some reason // this if check makes sure, that we don't decrease nextSegment. for some reason
// streamate playlists sometimes jump back. e.g. max sequence = 79 -> 80 -> 79 // streamate playlists sometimes jump back. e.g. max sequence = 79 -> 80 -> 79
lastSegment = lsp.seq; lastSegment = playlist.seq;
if(lastSegment + lsp.segments.size() > nextSegment) { if(lastSegment + playlist.segments.size() > nextSegment) {
nextSegment = lastSegment + lsp.segments.size(); nextSegment = lastSegment + playlist.segments.size();
} }
} }
} else { } else {
@ -170,8 +170,10 @@ public class HlsDownload extends AbstractHlsDownload {
private URL url; private URL url;
private Path file; private Path file;
private HttpClient client; private HttpClient client;
private SegmentPlaylist playlist;
public SegmentDownload(URL url, Path dir, HttpClient client, String prefix) { public SegmentDownload(SegmentPlaylist playlist, URL url, Path dir, HttpClient client, String prefix) {
this.playlist = playlist;
this.url = url; this.url = url;
this.client = client; this.client = client;
File path = new File(url.getPath()); File path = new File(url.getPath());
@ -185,10 +187,12 @@ public class HlsDownload extends AbstractHlsDownload {
for (int i = 1; i <= maxTries; i++) { for (int i = 1; i <= maxTries; i++) {
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build(); Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
Response response = client.execute(request); Response response = client.execute(request);
try ( InputStream in = null;
FileOutputStream fos = new FileOutputStream(file.toFile()); try (FileOutputStream fos = new FileOutputStream(file.toFile())) {
InputStream in = response.body().byteStream()) in = response.body().byteStream();
{ if(playlist.encrypted) {
in = new Crypto(playlist.encryptionKeyUrl, client).wrap(in);
}
byte[] b = new byte[1024 * 100]; byte[] b = new byte[1024 * 100];
int length = -1; int length = -1;
while( (length = in.read(b)) >= 0 ) { while( (length = in.read(b)) >= 0 ) {
@ -205,6 +209,9 @@ public class HlsDownload extends AbstractHlsDownload {
LOG.warn("Error while downloading segment on try {}", i); LOG.warn("Error while downloading segment on try {}", i);
} }
} finally { } finally {
if(in != null) {
in.close();
}
response.close(); response.close();
} }
} }

View File

@ -233,7 +233,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
private void downloadRecording(SegmentPlaylist lsp) throws IOException, InterruptedException { private void downloadRecording(SegmentPlaylist lsp) throws IOException, InterruptedException {
for (String segment : lsp.segments) { for (String segment : lsp.segments) {
URL segmentUrl = new URL(segment); URL segmentUrl = new URL(segment);
SegmentDownload segmentDownload = new SegmentDownload(segmentUrl, client); SegmentDownload segmentDownload = new SegmentDownload(lsp, segmentUrl, client);
byte[] segmentData = segmentDownload.call(); byte[] segmentData = segmentDownload.call();
writeSegment(segmentData); writeSegment(segmentData);
} }
@ -258,7 +258,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
skip--; skip--;
} else { } else {
URL segmentUrl = new URL(segment); URL segmentUrl = new URL(segment);
Future<byte[]> download = downloadThreadPool.submit(new SegmentDownload(segmentUrl, client)); Future<byte[]> download = downloadThreadPool.submit(new SegmentDownload(lsp, segmentUrl, client));
downloads.add(download); downloads.add(download);
} }
} }
@ -448,8 +448,10 @@ public class MergedHlsDownload extends AbstractHlsDownload {
private class SegmentDownload implements Callable<byte[]> { private class SegmentDownload implements Callable<byte[]> {
private URL url; private URL url;
private HttpClient client; private HttpClient client;
private SegmentPlaylist lsp;
public SegmentDownload(URL url, HttpClient client) { public SegmentDownload(SegmentPlaylist lsp, URL url, HttpClient client) {
this.lsp = lsp;
this.url = url; this.url = url;
this.client = client; this.client = client;
} }
@ -463,15 +465,18 @@ public class MergedHlsDownload extends AbstractHlsDownload {
try (Response response = client.execute(request)) { try (Response response = client.execute(request)) {
if(response.isSuccessful()) { if(response.isSuccessful()) {
byte[] segment = response.body().bytes(); byte[] segment = response.body().bytes();
if(lsp.encrypted) {
segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment);
}
return segment; return segment;
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
} }
} catch(Exception e) { } catch(Exception e) {
if (i == maxTries) { if (i == maxTries) {
LOG.warn("Error while downloading segment. Segment {} finally failed", url.getFile()); LOG.error("Error while downloading segment. Segment {} finally failed", url.getFile());
} else { } else {
LOG.warn("Error while downloading segment {} on try {}", url.getFile(), i, e); LOG.trace("Error while downloading segment {} on try {}", url.getFile(), i, e);
} }
if(model != null && !isModelOnline()) { if(model != null && !isModelOnline()) {
break; break;