视频加密是指对视频中的内容进行加密,可有效防止视频泄露和盗链问题,广泛用于在线教育及财经等领域。阿里云目前支持两种加密方式:阿里云私有加密(推荐)和HLS标准加密。本文介绍媒体处理HLS加密的原理和接入流程。

工作原理

媒体处理采用信封数据加密的方式加密视频。业务方调用阿里云密钥管理服务(KMS)生成数据密钥(DK)和信封数据密钥(EDK),然后利用数据密钥(DK)加密视频,并将加密后的文件和信封数据密钥(EDK)存储。播放器终端通过解密服务获取数据密钥(DK)请求解密播放视频。

说明
  • 数据密钥(DK)也称明文密钥,生成后用于视频加密。
  • 信封数据密钥(EDK)也称密文密钥,是通过信封加密技术保密后的密文数据密钥。主要用于解密数据密钥,得到明文数据密钥。
  • HLS加密要求用户自己保护数据密钥(DK)。

媒体处理的视频加密流程如下:

mts_wf_hls_encrypt
  1. 业务方开通阿里云媒体处理服务(MPS)、存储服务(OSS)、访问控制(RAM)、密钥管理服务(KMS)以及内容分发网络(CDN)(如未开通)。
    说明
    • OSS是阿里云提供的数据存储服务,媒体处理操作的媒体资源均存放在OSS的Bucket中。

    • RAM是阿里云提供的访问控制服务,业务方通过RAM授权MPS访问业务方KMS服务。

    • KMS是阿里云提供的安全管理服务,主要负责数据密钥的生产、加密、解密等工作。

    • CDN是阿里云提供的内容分发网络,在HLS加密流程中,CDN会动态修改M3U8文件中的解密URI并返回给播放端。

  2. 业务方授权媒体处理访问刚开通的阿里云密钥管理服务(KMS)。
    说明 授权访问是为了媒体处理在加密视频阶段调用KMS的GenerateDataKey接口生成数据密钥(DK)和信封数据密钥(EDK)。
  3. 业务方配置OSS输出Bucket域名为CDN加速域名,配置OSS域名CNAME及回源Host。
  4. 业务方创建视频加密工作流,传入OSS输出Bucket、Key URI等关键信息。

    Key URI是业务方的服务地址。媒体处理完成视频加密后的M3U8文件中会包含Key URI信息。

  5. 业务方上传需要加密的视频,上传过程中指定创建好的视频加密工作流。
  6. 视频上传完成后媒体处理自动触发加密转码。

    媒体处理调用GenerateDataKey接口生成数据密钥(DK)和信封数据密钥(EDK),并使用数据密钥(DK)加密视频。加密完成后,将业务方提供的Key URI与信封数据密钥(EDK)写入M3U8文件。

  7. 媒体处理将M3U8文件及ts文件存入OSS的输出Bucket中。

播放端解密播放HLS加密视频的流程如下:

mts_hls_decrypt
  1. 业务方搭建令牌服务,用于颁发令牌MtsHlsUriToken。
    注意 令牌服务指用于派发MtsHlsUriToken的服务。
  2. 业务方调用KMS解密接口搭建解密服务,用于解密视频,同时提供数据密钥(DK)给播放终端。
    注意 KMS返回Base64加密后的数据密钥给业务方。业务方需要将调用KMS接口获得的数据密钥Base64 Decode之后返回给播放终端。
  3. 业务方调用MPS的QueryMediaList接口获取视频M3U8文件的OSS地址,并将地址拼接MtsHlsUriToken后返回给播放终端。
  4. 播放终端携带MtsHlsUriToken、数据密钥向阿里云CDN请求播放地址,阿里云CDN改写M3U8文件,将业务方的Key URI与信封加密密钥返回播放终端。播放终端解密播放视频。

业务方实现概览

在完整的HLS加密视频和解密播放流程中,需要业务方自行实现的代码逻辑包括:

  • 创建加密工作流。
    说明 尽管控制台也可以创建加密工作流,但为了更完整高效地使用HLS标准加密服务,建议您集成服务端SDK后创建。
  • 搭建颁发及验证MtsHlsUriToken令牌服务,并校验解密令牌。推荐一个令牌只允许使用一次。
  • 调用KMS服务的解密接口搭建解密服务,并将调用KMS接口获得的明文密钥Base64 Decode之后返回播放终端。

接入准备

接入媒体处理的HLS加密功能需要完成以下准备:

  1. 开通相关阿里云服务。
    如果您尚未开通MPS,OSS,KMS, RAM,CDN服务,请先开通服务。
    1. 开通MPS服务。具体操作,请参见开通MPS服务
    2. 开通OSS服务。具体操作,请参见开通OSS服务
    3. 开通KMS服务。具体操作,请参见开通KMS服务
    4. 开通RAM服务并授权。具体操作,请参见创建RAM用户并授权
    5. 开通CDN服务。具体操作,请参见开通CDN服务
  2. 授权媒体处理访问KMS。
    1. 登录RAM访问控制台
    2. 单击授权进入新增授权页面。
    3. 授权主体搜索框搜索AliyunMtsDefaultRole,选择系统创建的可供媒体处理使用的角色。
    4. 选择权限下方的搜索框搜索KMS并选择AliyunKMSFullAccess,然后单击确定
    完成授权后,媒体处理可以访问您的KMS服务。收到加密视频请求时,媒体处理调取KMS接口获取数据密钥加密视频。
  3. 配置OSS输出Bucket域名及回源Host。具体操作,请参见配置加速域名。如已配置请忽略。
    说明 OSS域名可以手动输入阿里云OSS Bucket的外网域名(OSS外网域名可前往OSS控制台查看),如:exampleBucket****.oss-cn-hangzhou.aliyuncs.com ,也可以直接选择同账号下需要加速的OSS Bucket。不支持OSS内网域名。

加密视频

请按以下指引完成视频加密:

  1. 创建加密工作流。
    创建加密工作流需要集成阿里云SDK并添加媒体处理相关依赖。请根据您使用的开发语言选择查看对应代码示例。
    注意 创建加密工作流时必须传入业务方的Key URI地址,媒体处理加密视频时将该地址写入M3U8文件存储到OSS。Key URI示例:example.aliyundoc.com
    语言 集成SDK 加密工作流代码示例
    Java Java SDK安装 创建HLS标准加密工作流
    Python Python SDK安装 创建HLS标准加密工作流
    PHP PHP SDK安装 创建HLS标准加密工作流
    Node.js Node.js SDK安装 创建HLS标准加密工作流
  2. 上传视频触发加密转码。您可以通过媒体处理控制台或OSS控制台上传视频。具体操作请参见上传视频
    说明 由于加密工作流已经创建,上传视频时指定创建好的加密工作流即可自动触发媒体处理加密转码。
    加密完成后,可登录OSS控制台,在输出Bucket文件路径下查看M3U8文件。文件示例如下:
    #EXTM3U
         #EXT-X-VERSION:3
         #EXT-X-TARGETDURATION:5
         #EXT-X-MEDIA-SEQUENCE:0
         #EXT-X-KEY:METHOD=AES-128,URI="https://example.aliyundoc.com?Ciphertext=aabbccddeeff&MediaId=fbbf98691ea44b7c82dd75c5bc8b****"
         #EXTINF:4.127544,
         15029611683170-00001.ts
         #EXT-X-ENDLIST

    示例中,业务方配置的Key URI与媒体处理从KMS获取的信封数据密钥(EDK)包含在URI中。

播放HLS加密视频

请按以下指引完成视频解密播放:

  1. 搭建令牌服务。
    说明 令牌服务需要根据您的加密逻辑自行搭建,以达到更高的视频安全等级。
  2. 搭建解密服务。
    搭建一个本地HTTP服务,用于解密视频和获取解密密钥。媒体处理提供Java及Python代码示例。
    • Java代码示例

      Java SDK需要的依赖如下:

      Java示例代码如下:

      
      package com.aliyun.smallcode;
      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 AuthorizationServer {
      private static DefaultAcsClient client;
      static {
      String region = "";
      String accessKeyId = "";
      String accessKeySecret = "";
      client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret));
      }
      public class AuthorizationHandler implements HttpHandler {
      public void handle(HttpExchange httpExchange) throws IOException {
      String requestMethod = httpExchange.getRequestMethod();
      if(requestMethod.equalsIgnoreCase("GET")){
      //从URL中取得密文密钥
      String ciphertext = getCiphertext(httpExchange);
      if (null == ciphertext)
      return;
      //从KMS中解密出来,并Base64 decode
      byte[] key = decrypt(ciphertext);
      //设置header
      setHeader(httpExchange, key);
      //返回密钥
      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);
      }
      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;
      }
      }
      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");
      return null;
      }
      }
      }
      private void startService() throws IOException {
      HttpServerProvider provider = HttpServerProvider.provider();
      //监听端口8888,能同时接受10个请求
      HttpServer httpserver = provider.createHttpServer(new InetSocketAddress(8888), 10);
      httpserver.createContext("/", new AuthorizationHandler());
      httpserver.start();
      System.out.println("server started");
      }
      public static void main(String[] args) throws IOException {
      AuthorizationServer server = new AuthorizationServer();
      server.startService();
      }
      }
    • Python代码示例
      Python SDK需要的依赖如下:
      • pip install aliyun-python-sdk-core
      • pip install aliyun-python-sdk-kms
      • pip install aliyun-python-sdk-mts
      Python代码示例如下:
      
      # -*- coding: UTF-8 -*- 
      from BaseHTTPServer import BaseHTTPRequestHandler
      from aliyunsdkcore.client import AcsClient
      from aliyunsdkkms.request.v20160120 import DecryptRequest
      import cgi
      import json
      import base64
      import urlparse
      client = AcsClient("","","");
      class AuthorizationHandler(BaseHTTPRequestHandler):
      def do_GET(self):
      self.check()
      self.set_header()
      cipertext = self.get_cihpertext()
      plaintext = self.decrypt_cihpertext(cipertext)
      print plaintext
      key = base64.b64decode(plaintext)
      print key
      self.wfile.write(key)
      def do_POST(self):
      pass
      def check(self):
      #check MtsHlsUriToken, etc.
      pass
      def set_header(self):
      self.send_response(200)
      #cors
      self.send_header('Access-Control-Allow-Origin', '*')
      self.end_headers()
      def get_cihpertext(self):
      path = urlparse.urlparse(self.path)
      query = urlparse.parse_qs(path.query)
      return query.get('Ciphertext')[0]
      def decrypt_cihpertext(self, cipertext):
      request = DecryptRequest.DecryptRequest()
      request.set_CiphertextBlob(cipertext)
      response = client.do_action_with_exception(request)
      jsonResp = json.loads(response)
      return jsonResp["Plaintext"]
      if __name__ == '__main__':
      # Start a simple server, and loop forever
      from BaseHTTPServer import HTTPServer
      print "Starting server, use  to stop"
      server = HTTPServer(('127.0.0.1', 8888), AuthorizationHandler)
      server.serve_forever()
  3. 调用媒体处理的查询媒体-使用媒体ID接口获取播放地址。
    您可以通过开发者门户直接调用API或将API集成到服务中调用。
  4. 播放加密视频。
    您可以使用自己的播放器或阿里云播放器播放加密视频。
    • 如果使用非阿里云播放器,请自行实现播放逻辑。
    • 如果使用阿里云播放器,请按照阿里云播放器的要求获取令牌和鉴权信息后播放。详细介绍请参见视频播放
    您也可以借助一个在线播放器,测试HLS加密视频的播放。
    阿里云播放器用户诊断工具为例,请将获取的播放地址,填入对话框中,单击视频播放
    说明 通过浏览器DEBUG,可以看到播放器自动请求了鉴权服务器,获取解密密钥,并进行解密播放。

    使用阿里云播放器测试播放的内部流程解析如下:

    • 获取播放地址后,播放器把OSS域名替换为CDN域名,再拼接上参数MtsHlsUriToken作为请求解密密钥的令牌向CDN获请求播放地址。请求示例:https://example.aliyundoc.com/test_01.m3u8?MediaId=fbbf98691ea44b7c82dd75c5bc8b****&MtsHlsUriToken=<业务方颁发的令牌>
      注意

      如果您使用的是阿里云播放器,则无需自行拼接MtsHlsUriToken。如果您使用的是其它播放器,则需要自行拼接。

    • 收到请求后,阿里云CDN动态修改M3U8文件中的解密URI并返回修改后的地址给播放器,如原为https://example.aliyundoc.com?Ciphertext=aabbccddeeff&MediaId=fbbf98691ea44b7c82dd75c5bc8b****,修改后为 https://example.aliyundoc.com?Ciphertext=aabbccddeeff&MediaId=fbbf98691ea44b7c82dd75c5bc8b****&MtsHlsUriToken=<业务方颁发的令牌>

    • 播放器访问M3U8文件中EXT-X-KEY标签中的URI以获取解密密钥,此URI为业务方搭建的解密密钥接口。业务方调用KMS的Decrypt接口,并将获取的明文密钥Base64decode之后返回给播放器。播放器用数据密钥(DK)去解密加密过的ts文件进行播放。