HLS標(biāo)準(zhǔn)加密
HLS標(biāo)準(zhǔn)加密需要配合密鑰管理服務(wù)和令牌服務(wù)使用,本文為您介紹HLS標(biāo)準(zhǔn)加密的相關(guān)概念、準(zhǔn)備工作和接入流程。
HLS加密解密流程
上傳加密流程圖播放解密流程圖
使用密鑰管理服務(wù)會產(chǎn)生費(fèi)用,計費(fèi)詳情請參見KMS計費(fèi)說明。
令牌服務(wù)和解密服務(wù)需要自行搭建。
相關(guān)概念
密鑰管理服務(wù)
密鑰管理服務(wù)(Key Management Service,簡稱KMS),是一種安全管理服務(wù),主要負(fù)責(zé)數(shù)據(jù)密鑰的生產(chǎn)、加密、解密等工作。
訪問控制
訪問控制(Resource Access Management,簡稱RAM)是阿里云為客戶提供的用戶身份管理與資源訪問控制服務(wù)。
數(shù)據(jù)密鑰
數(shù)據(jù)密鑰(Data Key,簡稱DK)也稱明文密鑰。DK為加密數(shù)據(jù)使用的明文數(shù)據(jù)密鑰。
信封數(shù)據(jù)密鑰
信封數(shù)據(jù)密鑰(Enveloped Data Key,簡稱EDK)也稱密文密鑰。EDK為通過信封加密技術(shù)保密后的密文數(shù)據(jù)密鑰。
準(zhǔn)備工作
開通視頻點(diǎn)播服務(wù)并登錄視頻點(diǎn)播控制臺,開啟對應(yīng)服務(wù)區(qū)域的存儲空間,具體操作,請參見開通存儲管理。
在視頻點(diǎn)播控制臺上配置加速域名,并開啟該域名視頻相關(guān)中的HLS標(biāo)準(zhǔn)加密參數(shù)透傳,開啟后MtsHlsUriToken參數(shù)可以重寫。具體操作,請參見添加加速域名、HLS標(biāo)準(zhǔn)加密參數(shù)透傳。
登錄RAM訪問控制,獲取并保存AccessKey ID和AccessKey Secret。
開通密鑰管理服務(wù)并獲取Service Key。
說明Service Key是密鑰管理服務(wù)的一種加密主Key,接入標(biāo)準(zhǔn)加密的密鑰必須要使用該Service Key生成。
Service Key與視頻存儲的源站地域必須一致,例如:視頻存儲在華東2,則Service Key必須是在華東2創(chuàng)建。
登錄視頻點(diǎn)播控制臺,選擇 ,在標(biāo)準(zhǔn)加密頁面,創(chuàng)建Service Key。
創(chuàng)建成功后,需調(diào)用GenerateDataKey接口,請求參數(shù)
KeyId
傳入別名alias/acs/vod,請求后的返回參數(shù)KeyId
將用于后續(xù)的轉(zhuǎn)碼處理。
已搭建服務(wù)端SDK,具體操作,請參見服務(wù)端SDK安裝。
接入流程
添加加密模板和不轉(zhuǎn)碼模板。
HLS標(biāo)準(zhǔn)加密轉(zhuǎn)碼需要創(chuàng)建兩個轉(zhuǎn)碼模板:加密模板和不轉(zhuǎn)碼模板。
不轉(zhuǎn)碼模板在開啟對應(yīng)服務(wù)區(qū)域的存儲空間后會自動生成。
說明目前點(diǎn)播上傳視頻默認(rèn)都會自動觸發(fā)轉(zhuǎn)碼(自動觸發(fā)暫不支持HLS標(biāo)準(zhǔn)加密)),因此對于標(biāo)準(zhǔn)加密為防止自動觸發(fā)轉(zhuǎn)碼,需要先使用不轉(zhuǎn)碼模板上傳視頻(該類模板不會自動觸發(fā)轉(zhuǎn)碼),然后再調(diào)用提交媒體轉(zhuǎn)碼作業(yè)接口發(fā)起標(biāo)準(zhǔn)加密轉(zhuǎn)碼。
添加加密模板并保存加密模板ID,操作流程如下:
在配置管理區(qū)域,選擇 。
在添加轉(zhuǎn)碼模板組頁面,輸入模板組名稱。
在普通轉(zhuǎn)碼模板區(qū)域單擊添加模板開始創(chuàng)建轉(zhuǎn)碼模板。
在基本參數(shù)區(qū)域,選擇封裝格式為hls。
在視頻參數(shù)區(qū)域、音頻參數(shù)區(qū)域及條件轉(zhuǎn)碼參數(shù)區(qū)域,可根據(jù)實際需要配置相關(guān)參數(shù),各參數(shù)的含義及配置限制請參見音視頻轉(zhuǎn)碼。
在高級參數(shù)區(qū)域,開啟視頻加密,并勾選私有加密選項(系統(tǒng)默認(rèn)勾選,否則不加密)。
說明該模板在調(diào)用提交媒體轉(zhuǎn)碼作業(yè)接口時,通過TemplateGroupId參數(shù)傳遞,如此視頻點(diǎn)播將按照設(shè)置的模板和傳遞的密鑰信息進(jìn)行標(biāo)準(zhǔn)加密轉(zhuǎn)碼。
單擊保存,自動返回轉(zhuǎn)碼模板組頁面,獲取并自行保存加密模板ID。
RAM授權(quán)。
使用RAM服務(wù)給視頻點(diǎn)播授權(quán)訪問業(yè)務(wù)方密鑰管理服務(wù)的權(quán)限,進(jìn)入RAM授權(quán)頁面,單擊同意授權(quán)。
搭建密鑰管理服務(wù),封裝阿里云密鑰管理服務(wù)(KMS)。
調(diào)用GenerateDataKey接口生成一個AES_128密鑰,該接口只需要傳KeyId(Service Key)和KeySpec(固定為:AES_128)即可,其他參數(shù)不用傳,否則可能加密失敗。
調(diào)用成功后保存返回參數(shù)
CiphertextBlob
(密文密鑰)的值。說明使用密鑰會產(chǎn)生費(fèi)用,具體費(fèi)用說明,請參見API調(diào)用費(fèi)用。
搭建令牌頒發(fā)服務(wù),生成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生成方式,無需以下參數(shù) //加密字符串,用戶自行定義 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(""); } /** * 根據(jù)傳遞的參數(shù)生成令牌 * 說明: * 1、參數(shù)可以是業(yè)務(wù)方的用戶ID、播放終端類型等信息 * 2、調(diào)用令牌接口時生成令牌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), "_"); //設(shè)置30S后,該token過期,過期時間可以自行調(diào)整 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的使用次數(shù) saveToken(token); return token; } /** * 驗證token的有效性 * 說明: * 1、解密接口在返回播放密鑰前,需要先校驗Token的合法性和有效性 * 2、強(qiáng)烈建議同時校驗Token的過期時間以及Token的有效使用次數(shù) * @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的有效性,業(yè)務(wù)方可自行實現(xiàn) Token dbToken = getToken(token); //判斷是否已經(jīng)使用過該token if (dbToken == null || dbToken.useCount > 0) { return false; } //獲取到業(yè)務(wù)屬性信息,用于校驗 String businessInfo = base.substring(0, base.lastIndexOf("_")); String[] items = businessInfo.split("_"); //校驗業(yè)務(wù)信息的合法性,業(yè)務(wù)方實現(xiàn) return validateInfo(items); } /** * 保存Token到DB * 業(yè)務(wù)方自行實現(xiàn) * * @param token */ public void saveToken(String token) { //TODO 存儲Token } /** * 查詢Token * 業(yè)務(wù)方自行實現(xiàn) * * @param token */ public Token getToken(String token) { //TODO 從DB 獲取Token信息,用于校驗有效性和合法性 return null; } /** * 校驗業(yè)務(wù)信息的有效性,業(yè)務(wù)方可自行實現(xiàn) * * @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信息,業(yè)務(wù)方可提供更多信息,這里僅僅給出示例 */ class Token { //Token的有效使用次數(shù),分布式環(huán)境需要注意同步修改問題 int useCount; //token內(nèi)容 String token; }}
搭建解密服務(wù)。
重要解密服務(wù)在播放視頻前就需要啟動,否則視頻無法正常解密。
解密密鑰EDK(密文密鑰),調(diào)用Decrypt接口進(jìn)行解密。如果業(yè)務(wù)方需要對解密接口進(jìn)行安全驗證,則需要提供令牌生成服務(wù),生成的令牌能夠在解密服務(wù)中被解析驗證。
解密接口返回的數(shù)據(jù),是GenerateDataKey生成的兩種密鑰中的明文密鑰(PlainText)經(jīng)過base64decode之后的數(shù)據(jù)。
Java示例代碼以及示例代碼需要手動變更的地方如下所示:
region:填寫地域,例如華東2(上海),填寫
cn-shanghai
。AccessKey:填寫對應(yīng)賬號的AccessKey ID和AccessKey Secret。
httpserver:根據(jù)需求選擇服務(wù)啟動的端口號。
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的區(qū)域,必須與視頻對應(yīng)區(qū)域 String region = ""; // 訪問KMS的授權(quán)AccessKey信息 // 阿里云賬號AccessKey擁有所有API的訪問權(quán)限,建議您使用RAM用戶進(jìn)行API訪問或日常運(yùn)維。 // 強(qiáng)烈建議不要把AccessKey ID和AccessKey Secret保存到工程代碼里,否則可能導(dǎo)致AccessKey泄露,威脅您賬號下所有資源的安全。 // 本示例通過從環(huán)境變量中讀取AccessKey,來實現(xiàn)API訪問的身份驗證。運(yùn)行代碼示例前,請配置環(huán)境變量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、調(diào)用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); //設(shè)置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); } /** * 調(diào)用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 業(yè)務(wù)方實現(xiàn)令牌有效性校驗 return true; } /** * 從URL中獲取密文密鑰參數(shù) * @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參數(shù) * * @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; } } } /** * 服務(wù)啟動 * * @throws IOException */ private void serviceBootStrap() throws IOException { HttpServerProvider provider = HttpServerProvider.provider(); //監(jiān)聽端口可以自定義,能同時接受最多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(); }}
上傳視頻。
使用不轉(zhuǎn)碼模板創(chuàng)建視頻上傳憑證和地址。控制臺具體操作,請參見控制臺上傳;服務(wù)端接口上傳,請參見獲取音視頻上傳地址和憑證。
接收上傳完成回調(diào)消息。
通過設(shè)置事件通知配置設(shè)置回調(diào),通過查詢事件通知配置查詢回調(diào)消息,當(dāng)接收到視頻上傳完成的回調(diào)消息,則表明文件已經(jīng)上傳到視頻點(diǎn)播。
發(fā)起標(biāo)準(zhǔn)加密轉(zhuǎn)碼。
調(diào)用提交媒體轉(zhuǎn)碼作業(yè)接口發(fā)起標(biāo)準(zhǔn)加密轉(zhuǎn)碼。
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的訪問權(quán)限,建議您使用RAM用戶進(jìn)行API訪問或日常運(yùn)維。 // 強(qiáng)烈建議不要把AccessKey ID和AccessKey Secret保存到工程代碼里,否則可能導(dǎo)致AccessKey泄露,威脅您賬號下所有資源的安全。 // 本示例通過從環(huán)境變量中讀取AccessKey,來實現(xiàn)API訪問的身份驗證。運(yùn)行代碼示例前,請配置環(huán)境變量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 { // 點(diǎn)播服務(wù)接入?yún)^(qū)域 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(); } } }
驗證加密轉(zhuǎn)碼是否成功。
您可以登錄視頻點(diǎn)播控制臺查看該視頻的視頻地址,通過以下三種方式來逐步判斷標(biāo)準(zhǔn)加密是否成功。
當(dāng)視頻加密轉(zhuǎn)碼后,如果該視頻的視頻地址只有一個M3U8格式的視頻地址,那么該視頻狀態(tài)為轉(zhuǎn)碼失敗。
當(dāng)視頻加密轉(zhuǎn)碼后,如果視頻不只有M3U8格式的輸出(例如還存在格式為MP4的原始文件),只需查看M3U8格式后是否帶有標(biāo)準(zhǔn)加密,一般情況下,如果存在則表明標(biāo)準(zhǔn)加密已成功。
如果以上兩種方式都不能判斷,那么可以將帶有加密標(biāo)志的M3U8文件的地址拷貝出來,使用
curl -v "M3U8文件地址"
,查看獲取到的M3U8內(nèi)容是否存在URI="<業(yè)務(wù)方在發(fā)起標(biāo)準(zhǔn)加密時傳遞的解密地址,即加密配置 EncryptConfig中的DecryptKeyUri參數(shù)值>"
關(guān)鍵信息,有則表明為標(biāo)準(zhǔn)加密且加密成功。
播放流程
獲取視頻的播放地址和憑證。
傳入認(rèn)證信息。
獲取M3U8文件地址后,播放器會解析M3U8文件中的EXT-X-KEY標(biāo)簽中的URI并訪問,從而獲取到帶密文密鑰的解密接口URI,此URI為您發(fā)起標(biāo)準(zhǔn)加密時傳遞的加密配置 EncryptConfig中的
DecryptKeyUri
參數(shù)值。若只允許合法用戶才可以訪問,那么需要播放器在獲取解密密鑰時攜帶您承認(rèn)的認(rèn)證信息,認(rèn)證信息可以通過MtsHlsUriToken參數(shù)傳入。
示例:
視頻的播放地址為:
https://demo.aliyundoc.com/encrypt-stream****-hd.m3u8
,則請求時需要攜帶MtsHlsUriToken
參數(shù)傳入。最終請求地址為:
https://demo.aliyundoc.com/encrypt-stream****-hd.m3u8?MtsHlsUriToken=<令牌>
解密地址為:
https://demo.aliyundoc.com?Ciphertext=ZjJmZGViNzUtZWY1Mi00Y2RlLTk3MTMtOT****
最終解密請求地址為:
https://demo.aliyundoc.com?Ciphertext=ZjJmZGViNzUtZWY1Mi00Y2RlLTk3MTMtOT****&MtsHlsUriToken=<頒發(fā)的令牌>
。
播放。
播放器在解析到解密地址URI時會自動請求解密接口獲取解密密鑰,拿到解密密鑰去解密加密過的ts文件進(jìn)行播放。