From ca2ceb7f43710e582cb5291df01ba984a9895c8f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 12 Apr 2019 19:33:18 +0200 Subject: [PATCH] Add support for HLS AES encryption --- .../download/AbstractHlsDownload.java | 10 +++ .../java/ctbrec/recorder/download/Crypto.java | 63 +++++++++++++++++++ .../ctbrec/recorder/download/HlsDownload.java | 39 +++++++----- .../recorder/download/MergedHlsDownload.java | 15 +++-- 4 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 common/src/main/java/ctbrec/recorder/download/Crypto.java diff --git a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index 375a700f..1646794e 100644 --- a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -24,6 +24,7 @@ import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; +import com.iheartradio.m3u8.data.EncryptionData; import com.iheartradio.m3u8.data.MediaPlaylist; import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.TrackData; @@ -87,6 +88,12 @@ public abstract class AbstractHlsDownload implements Download { lsp.totalDuration += trackData.getTrackInfo().duration; lsp.lastSegDuration = trackData.getTrackInfo().duration; 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; } @@ -155,6 +162,9 @@ public abstract class AbstractHlsDownload implements Download { public float lastSegDuration = 0; public float targetDuration = 0; public List segments = new ArrayList<>(); + public boolean encrypted = false; + public String encryptionMethod = "AES-128"; + public String encryptionKeyUrl; public SegmentPlaylist(String url) { this.url = url; diff --git a/common/src/main/java/ctbrec/recorder/download/Crypto.java b/common/src/main/java/ctbrec/recorder/download/Crypto.java new file mode 100644 index 00000000..480b3c37 --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/download/Crypto.java @@ -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); + } +} diff --git a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java index 2207c4b6..d470d1d4 100644 --- a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -78,28 +78,28 @@ public class HlsDownload extends AbstractHlsDownload { int nextSegment = 0; int waitFactor = 1; while(running) { - SegmentPlaylist lsp = getNextSegments(segments); - if(nextSegment > 0 && lsp.seq > nextSegment) { + SegmentPlaylist playlist = getNextSegments(segments); + if(nextSegment > 0 && playlist.seq > nextSegment) { // TODO switch to a lower bitrate/resolution ?!? 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; - for (String segment : lsp.segments) { + int skip = nextSegment - playlist.seq; + for (String segment : playlist.segments) { if(skip > 0) { skip--; } else { URL segmentUrl = new URL(segment); 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(); } } long wait = 0; - if(lastSegment == lsp.seq) { + if(lastSegment == playlist.seq) { // 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); } else { // 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 // streamate playlists sometimes jump back. e.g. max sequence = 79 -> 80 -> 79 - lastSegment = lsp.seq; - if(lastSegment + lsp.segments.size() > nextSegment) { - nextSegment = lastSegment + lsp.segments.size(); + lastSegment = playlist.seq; + if(lastSegment + playlist.segments.size() > nextSegment) { + nextSegment = lastSegment + playlist.segments.size(); } } } else { @@ -170,8 +170,10 @@ public class HlsDownload extends AbstractHlsDownload { private URL url; private Path file; 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.client = client; File path = new File(url.getPath()); @@ -185,10 +187,12 @@ public class HlsDownload extends AbstractHlsDownload { for (int i = 1; i <= maxTries; i++) { Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build(); Response response = client.execute(request); - try ( - FileOutputStream fos = new FileOutputStream(file.toFile()); - InputStream in = response.body().byteStream()) - { + InputStream in = null; + try (FileOutputStream fos = new FileOutputStream(file.toFile())) { + in = response.body().byteStream(); + if(playlist.encrypted) { + in = new Crypto(playlist.encryptionKeyUrl, client).wrap(in); + } byte[] b = new byte[1024 * 100]; int length = -1; while( (length = in.read(b)) >= 0 ) { @@ -205,6 +209,9 @@ public class HlsDownload extends AbstractHlsDownload { LOG.warn("Error while downloading segment on try {}", i); } } finally { + if(in != null) { + in.close(); + } response.close(); } } diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index d823f299..9ea9efca 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -233,7 +233,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { private void downloadRecording(SegmentPlaylist lsp) throws IOException, InterruptedException { for (String segment : lsp.segments) { URL segmentUrl = new URL(segment); - SegmentDownload segmentDownload = new SegmentDownload(segmentUrl, client); + SegmentDownload segmentDownload = new SegmentDownload(lsp, segmentUrl, client); byte[] segmentData = segmentDownload.call(); writeSegment(segmentData); } @@ -258,7 +258,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { skip--; } else { URL segmentUrl = new URL(segment); - Future download = downloadThreadPool.submit(new SegmentDownload(segmentUrl, client)); + Future download = downloadThreadPool.submit(new SegmentDownload(lsp, segmentUrl, client)); downloads.add(download); } } @@ -448,8 +448,10 @@ public class MergedHlsDownload extends AbstractHlsDownload { private class SegmentDownload implements Callable { private URL url; 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.client = client; } @@ -463,15 +465,18 @@ public class MergedHlsDownload extends AbstractHlsDownload { try (Response response = client.execute(request)) { if(response.isSuccessful()) { byte[] segment = response.body().bytes(); + if(lsp.encrypted) { + segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment); + } return segment; } else { throw new HttpException(response.code(), response.message()); } } catch(Exception e) { 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 { - 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()) { break;