HLS標準加密需要配合Key Management Service和令牌服務使用,本文為您介紹HLS標準加密的相關概念、準備工作和接入流程。
HLS加密解密流程
上傳加密流程圖播放解密流程圖
使用Key Management Service會產生費用,計費詳情請參見KMS計費說明。
令牌服務和解密服務需要自行搭建。
相關概念
Key Management Service
Key Management Service(Key Management Service,簡稱KMS),是一種安全管理服務,主要負責資料密鑰的生產、加密、解密等工作。
存取控制
存取控制(Resource Access Management,簡稱RAM)是阿里雲為客戶提供的使用者身份管理與資源存取控制服務。
資料密鑰
資料密鑰(Data Key,簡稱DK)也稱清除金鑰。DK為加密資料使用的明文資料密鑰。
信封資料密鑰
信封資料密鑰(Enveloped Data Key,簡稱EDK)也稱密文密鑰。EDK為通過信封加密技術保密後的密文資料密鑰。
準備工作
開通ApsaraVideo for VOD服務並登入ApsaraVideo for VOD控制台,開啟對應服務地區的儲存空間,具體操作,請參見步驟一:開通儲存管理。
在ApsaraVideo for VOD控制台上配置加速網域名稱,並開啟該網域名稱視頻相關中的HLS標準加密參數透傳,開啟後MtsHlsUriToken參數可以重寫。具體操作,請參見添加加速網域名稱、HLS標準加密參數透傳。
登入RAM存取控制,擷取並儲存AccessKey ID和AccessKey Secret。
開通Key Management Service並擷取Service Key。
說明Service Key是Key Management Service的一種加密主Key,接入標準加密的密鑰必須要使用該Service Key產生。
Service Key與視頻儲存的來源站點地區必須一致,例如:視頻儲存在華東2,則Service Key必須是在華東2建立。
登入ApsaraVideo for VOD控制台,選擇 ,在標準加密頁面,建立Service Key。
建立成功後,需調用GenerateDataKey介面,請求參數
KeyId
傳入別名alias/acs/vod,請求後的返回參數KeyId
將用於後續的轉碼處理。
已搭建服務端SDK,具體操作,請參見服務端SDK安裝。
接入流程
添加加密模板和不轉碼模板。
HLS標準加密轉碼需要建立兩個轉碼模板:加密模板和不轉碼模板。
不轉碼模板在開啟對應服務地區的儲存空間後會自動產生。
說明目前點播上傳視頻預設都會自動觸發轉碼(自動觸發暫不支援HLS標準加密)),因此對於標準加密為防止自動觸發轉碼,需要先使用不轉碼模板上傳視頻(該類模板不會自動觸發轉碼),然後再調用提交媒體轉碼作業介面發起標準加密轉碼。
添加加密模板並儲存加密模板ID,操作流程如下:
RAM授權。
使用RAM服務給ApsaraVideo for VOD授權訪問業務方Key Management Service的許可權,進入RAM授權頁面,單擊同意授權。
搭建Key Management Service,封裝阿里雲Key Management Service(KMS)。
調用GenerateDataKey介面產生一個AES_128密鑰,該介面只需要傳KeyId(Service Key)和KeySpec(固定為:AES_128)即可,其他參數不用傳,否則可能加密失敗。
調用成功後儲存返回參數
CiphertextBlob
(密文密鑰)的值。說明使用密鑰會產生費用,具體費用說明,請參見API調用費用。
搭建令牌頒發服務,產生MtsHlsUriToken。
Java範例程式碼以及範例程式碼需要手動變更的地方如下所示:
ENCRYPT_KEY:加密字串,長度為16,使用者自行定義。
INIT_VECTOR:自訂字串,長度為16,不能含有特殊字元。
playToken.generateToken(""):自訂字串,長度為16。
最終代碼所產生的Token即是MtsHlsUriToken。
import com.sun.deploy.util.StringUtils; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Arrays; public class PlayToken { //非AES產生方式,無需以下參數 //加密字串,使用者自行定義 private static String ENCRYPT_KEY = ""; //長度為16的自訂字串,不能有特殊字元 private static String INIT_VECTOR = ""; public static void main(String[] args) throws Exception { PlayToken playToken = new PlayToken(); playToken.generateToken(""); } /** * 根據傳遞的參數產生令牌 * 說明: * 1、參數可以是業務方的使用者ID、播放終端類型等資訊 * 2、調用令牌介面時產生令牌Token * @param args * @return */ public String generateToken(String... args) throws Exception { if (null == args || args.length <= 0) { return null; } String base = StringUtils.join(Arrays.asList(args), "_"); //設定30S後,該token到期,到期時間可以自行調整 long expire = System.currentTimeMillis() + 30000L; //自訂字串,base的最終長度為16位字元(此例中,時間戳記佔13位,底線(_)佔1位,則還需傳入2位字元。實際配置時也可按需全部更改,最終保證base為16位字串即可。) base += "_" + expire; //產生token String token = encrypt(base, ENCRYPT_KEY); System.out.println(token); //儲存token,用於解密時校正token的有效性,例如:到期時間、token的使用次數 saveToken(token); return token; } /** * 驗證token的有效性 * 說明: * 1、解密介面在返回播放密鑰前,需要先校正Token的合法性和有效性 * 2、強烈建議同時校正Token的到期時間以及Token的有效使用次數 * @param token * @return * @throws Exception */ public boolean validateToken(String token) throws Exception { if (null == token || "".equals(token)) { return false; } String base = decrypt(token, ENCRYPT_KEY); //先校正token的有效時間 Long expireTime = Long.valueOf(base.substring(base.lastIndexOf("_") + 1)); if (System.currentTimeMillis() > expireTime) { return false; } //從DB擷取token資訊,判斷token的有效性,業務方可自行實現 Token dbToken = getToken(token); //判斷是否已經使用過該token if (dbToken == null || dbToken.useCount > 0) { return false; } //擷取到業務屬性資訊,用於校正 String businessInfo = base.substring(0, base.lastIndexOf("_")); String[] items = businessInfo.split("_"); //校正商務資訊的合法性,業務方實現 return validateInfo(items); } /** * 儲存Token到DB * 業務方自行實現 * * @param token */ public void saveToken(String token) { //TODO 儲存Token } /** * 查詢Token * 業務方自行實現 * * @param token */ public Token getToken(String token) { //TODO 從DB 擷取Token資訊,用於校正有效性和合法性 return null; } /** * 校正商務資訊的有效性,業務方可自行實現 * * @param infos * @return */ public boolean validateInfo(String... infos) { //TODO 校正資訊的有效性,例如UID是否有效等 return true; } /** * AES加密產生Token * * @param key * @param value * @return * @throws Exception */ public String encrypt(String value, String key) throws Exception { IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.ENCRYPT_MODE, skeySpec, e); byte[] encrypted = cipher.doFinal(value.getBytes()); return Base64.encodeBase64String(encrypted); } /** * AES解密token * * @param key * @param encrypted * @return * @throws Exception */ public String decrypt(String encrypted, String key) throws Exception { IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.DECRYPT_MODE, skeySpec, e); byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted)); return new String(original); } /** * Token資訊,業務方可提供更多資訊,這裡僅僅給出樣本 */ class Token { //Token的有效使用次數,分布式環境需要注意同步修改問題 int useCount; //token內容 String token; }}
搭建解密服務。
重要解密服務在播放視頻前就需要啟動,否則視頻無法正常解密。
解密密鑰EDK(密文密鑰),調用Decrypt介面進行解密。如果業務方需要對解密介面進行安全驗證,則需要提供令牌產生服務,產生的令牌能夠在解密服務中被解析驗證。
解密介面返回的資料,是GenerateDataKey產生的兩種密鑰中的清除金鑰(PlainText)經過base64decode之後的資料。
Java範例程式碼以及範例程式碼需要手動變更的地方如下所示:
region:填寫地區,例如華東2(上海),填寫
cn-shanghai
。AccessKey:填寫對應帳號的AccessKey ID和AccessKey Secret。
httpserver:根據需求選擇服務啟動的連接埠號碼。
import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.http.ProtocolType; import com.aliyuncs.kms.model.v20160120.DecryptRequest; import com.aliyuncs.kms.model.v20160120.DecryptResponse; import com.aliyuncs.profile.DefaultProfile; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.spi.HttpServerProvider; import org.apache.commons.codec.binary.Base64; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URI;import java.util.regex.Matcher; import java.util.regex.Pattern; public class HlsDecryptServer { private static DefaultAcsClient client; static { //KMS的地區,必須與視頻對應地區 String region = ""; // 訪問KMS的授權AccessKey資訊 // 阿里雲帳號AccessKey擁有所有API的存取權限,建議您使用RAM使用者進行API訪問或日常營運。 // 強烈建議不要把AccessKey ID和AccessKey Secret儲存到工程代碼裡,否則可能導致AccessKey泄露,威脅您帳號下所有資源的安全。 // 本樣本通過從環境變數中讀取AccessKey,來實現API訪問的身分識別驗證。運行程式碼範例前,請配置環境變數ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。 String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret)); } /** * 說明: * 1、接收解密請求,擷取密文密鑰和令牌Token * 2、調用KMS decrypt介面擷取清除金鑰 * 3、將清除金鑰base64decode返回 */ public class HlsDecryptHandler implements HttpHandler { /** * 處理解密請求 * @param httpExchange * @throws IOException */ public void handle(HttpExchange httpExchange) throws IOException { String requestMethod = httpExchange.getRequestMethod(); if ("GET".equalsIgnoreCase(requestMethod)) { //校正token的有效性 String token = getMtsHlsUriToken(httpExchange); boolean validRe = validateToken(token); if (!validRe) { return; } //從URL中取得密文密鑰 String ciphertext = getCiphertext(httpExchange); if (null == ciphertext) return; //從KMS中解密出來,並Base64 decode byte[] key = decrypt(ciphertext); //設定header setHeader(httpExchange, key); //返回base64decode之後的密鑰 OutputStream responseBody = httpExchange.getResponseBody(); responseBody.write(key); responseBody.close(); } } private void setHeader(HttpExchange httpExchange, byte[] key) throws IOException { Headers responseHeaders = httpExchange.getResponseHeaders(); responseHeaders.set("Access-Control-Allow-Origin", "*"); httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, key.length); } /** * 調用KMS decrypt介面解密,並將明文base64decode * @param ciphertext * @return */ private byte[] decrypt(String ciphertext) { DecryptRequest request = new DecryptRequest(); request.setCiphertextBlob(ciphertext); request.setProtocol(ProtocolType.HTTPS); try { DecryptResponse response = client.getAcsResponse(request); String plaintext = response.getPlaintext(); //注意:需要base64 decode return Base64.decodeBase64(plaintext); } catch (ClientException e) { e.printStackTrace(); return null; } } /** * 校正令牌有效性 * @param token * @return */ private boolean validateToken(String token) { if (null == token || "".equals(token)) { return false; } //TODO 業務方實現令牌有效性校正 return true; } /** * 從URL中擷取密文密鑰參數 * @param httpExchange * @return */ private String getCiphertext(HttpExchange httpExchange) { URI uri = httpExchange.getRequestURI(); String queryString = uri.getQuery(); String pattern = "CipherText=(\\w*)"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(queryString); if (m.find()) return m.group(1); else { System.out.println("Not Found CipherText Param"); return null; } } /** * 擷取Token參數 * * @param httpExchange * @return */ private String getMtsHlsUriToken(HttpExchange httpExchange) { URI uri = httpExchange.getRequestURI(); String queryString = uri.getQuery(); String pattern = "MtsHlsUriToken=(\\w*)"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(queryString); if (m.find()) return m.group(1); else { System.out.println("Not Found MtsHlsUriToken Param"); return null; } } } /** * 服務啟動 * * @throws IOException */ private void serviceBootStrap() throws IOException { HttpServerProvider provider = HttpServerProvider.provider(); //監聽連接埠可以自訂,能同時接受最多30個請求 HttpServer httpserver = provider.createHttpServer(new InetSocketAddress(8099), 30); httpserver.createContext("/", new HlsDecryptHandler()); httpserver.start(); System.out.println("hls decrypt server started"); } public static void main(String[] args) throws IOException { HlsDecryptServer server = new HlsDecryptServer(); server.serviceBootStrap(); }}
上傳視頻。
使用不轉碼模板建立視頻上傳憑證和地址。控制台具體操作,請參見通過ApsaraVideo for VOD控制台上傳檔案;服務端介面上傳,請參見擷取音視頻上傳地址和憑證。
接收上傳完成回調訊息。
發起標準加密轉碼。
調用提交媒體轉碼作業介面發起標準加密轉碼。
Java範例程式碼以及範例程式碼需要手動變更的地方如下所示:
request.setTemplateGroupId(""):傳入加密模板ID。
request.setVideoId(""):傳入的視訊ID。
encryptConfig.put("CipherText",""):傳入步驟三擷取的CiphertextBlob值。
encryptConfig.put("DecryptKeyUri",""):傳入播放地址、
CiphertextBlob
值以及MtsHlsUriToken
。以在本地的8099連接埠為例,播放地址為:http://172.16.0.1:8099?CipherText=CiphertextBlob值&MtsHlsUriToken=MtsHlsUriToken值
。
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.profile.DefaultProfile; import com.aliyuncs.vod.model.v20170321.SubmitTranscodeJobsRequest; import com.aliyuncs.vod.model.v20170321.SubmitTranscodeJobsResponse; public class SubmitTranscodeJobs { // 阿里雲帳號AccessKey擁有所有API的存取權限,建議您使用RAM使用者進行API訪問或日常營運。 // 強烈建議不要把AccessKey ID和AccessKey Secret儲存到工程代碼裡,否則可能導致AccessKey泄露,威脅您帳號下所有資源的安全。 // 本樣本通過從環境變數中讀取AccessKey,來實現API訪問的身分識別驗證。運行程式碼範例前,請配置環境變數ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。 private static String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); private static String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); public static SubmitTranscodeJobsResponse submitTranscodeJobs(DefaultAcsClient client) throws Exception{ SubmitTranscodeJobsRequest request = new SubmitTranscodeJobsRequest(); request.setTemplateGroupId(""); request.setVideoId(""); JSONObject encryptConfig = new JSONObject(); encryptConfig.put("CipherText",""); encryptConfig.put("DecryptKeyUri",""); encryptConfig.put("KeyServiceType","KMS"); request.setEncryptConfig(encryptConfig.toJSONString()); return client.getAcsResponse(request); } public static void main(String[] args) throws ClientException { // 點播服務接入地區 String regionId = "cn-shanghai"; DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); DefaultAcsClient client = new DefaultAcsClient(profile); SubmitTranscodeJobsResponse response; try { response = submitTranscodeJobs(client); System.out.println("RequestId is:"+response.getRequestId()); System.out.println("TranscodeTaskId is:"+response.getTranscodeTaskId()); System.out.println("TranscodeJobs is:"+ JSON.toJSON(response.getTranscodeJobs())); } catch (Exception e) { e.printStackTrace(); } } }
驗證加密轉碼是否成功。
您可以登入ApsaraVideo for VOD控制台查看該視頻的視頻地址,通過以下三種方式來逐步判斷標準加密是否成功。
當視頻加密轉碼後,如果該視頻的視頻地址只有一個M3U8格式的視頻地址,那麼該視頻狀態為轉碼失敗。
當視頻加密轉碼後,如果視頻不只有M3U8格式的輸出(例如還存在格式為MP4的原始檔案),只需查看M3U8格式後是否帶有標準加密,一般情況下,如果存在則表明標準加密已成功。
如果以上兩種方式都不能判斷,那麼可以將帶有加密標誌的M3U8檔案的地址拷貝出來,使用
curl -v "M3U8檔案地址"
,查看擷取到的M3U8內容是否存在URI="<業務方在發起標準加密時傳遞的解密地址,即加密配置 EncryptConfig中的DecryptKeyUri參數值>"
關鍵資訊,有則表明為標準加密且加密成功。
播放流程
擷取視頻的播放地址和憑證。
傳入認證資訊。
擷取M3U8檔案地址後,播放器會解析M3U8檔案中的EXT-X-KEY標籤中的URI並訪問,從而擷取到帶密文密鑰的解密介面URI,此URI為您發起標準加密時傳遞的加密配置 EncryptConfig中的
DecryptKeyUri
參數值。若只允許合法使用者才可以訪問,那麼需要播放器在擷取解密密鑰時攜帶您承認的認證資訊,認證資訊可以通過MtsHlsUriToken參數傳入。
樣本:
視頻的播放地址為:
https://demo.aliyundoc.com/encrypt-stream****-hd.m3u8
,則請求時需要攜帶MtsHlsUriToken
參數傳入。最終請求地址為:
https://demo.aliyundoc.com/encrypt-stream****-hd.m3u8?MtsHlsUriToken=<令牌>
解密地址為:
https://demo.aliyundoc.com?Ciphertext=ZjJmZGViNzUtZWY1Mi00Y2RlLTk3MTMtOT****
最終解密請求地址為:
https://demo.aliyundoc.com?Ciphertext=ZjJmZGViNzUtZWY1Mi00Y2RlLTk3MTMtOT****&MtsHlsUriToken=<頒發的令牌>
。
播放。
播放器在解析到解密地址URI時會自動請求解密介面擷取解密密鑰,拿到解密密鑰去解密加密過的ts檔案進行播放。