HTTP Live Streaming (HLS) encryption must be used together with Key Management Service (KMS) and the token service. This topic describes the terms, preparations, and procedures of HLS encryption.

Terms

  • KMS

    KMS is a security management service that is used to generate, encrypt, and decrypt data keys.

  • Resource access management (RAM)

    RAM allows you to manage user identities and control access to resources.

  • Data key (DK)

    DK is called the plaintext key and is used to encrypt data.

  • Enveloped data key (EDK)

    EDK is called the ciphertext key and is generated by using envelope encryption.

Preparations

  1. Activate ApsaraVideo VOD and log on to the ApsaraVideo VOD console to enable the bucket in your region. For more information, see Enable storage management.
  2. In the ApsaraVideo VOD console, configure an accelerated domain name and turn on Parameter Pass-through for HLS Encryption in Video Related for the domain name. After you enable this feature, the MtsHlsUriToken parameter can be configured. For more information, see Add a domain name for CDN and Parameter pass-through for HLS encryption.
  3. Log on to the RAM console. Obtain and record the AccessKey ID and AccessKey secret.
  4. Activate KMS and log on to the KMS console. Submit a ticket to create a service key. A service key must be created in the same region as the origin server where videos are stored. For example, if videos are stored in the China (Shanghai) region, the service key must be created in the China (Shanghai) region. After the service key is created, you can find the service key, which has an alias that includes vod, in the KMS console. The following figure shows an example of a service key.Service Key
    Note A service key is a primary encryption key used in KMS. You must use the service key to generate keys for HLS encryption. You cannot create a service key in the ApsaraVideo VOD console.
  5. Build a server SDK that matches your needs. For more information, see Server SDK installation.

Procedure

  1. Add an encryption template and a non-transcoding template.

    HLS encryption and transcoding require two templates: an encryption template and a non-transcoding template.

    A non-transcoding template is automatically generated after you enable the bucket in your region.
    Notice By default, ApsaraVideo VOD automatically transcodes uploaded videos. Automatic transcoding does not support HLS encryption. To prevent automatic transcoding when you use HLS encryption, you must use the non-transcoding template to upload videos. Then, you can call the SubmitTranscodeJobs operation to start HLS encryption and transcoding.

    To create an encryption template and record the template ID, perform the following steps:

    1. Log on to the ApsaraVideo VOD console.
    2. In the left-side navigation pane, find Configuration Management and choose Media Processing > Transcoding Template Groups > Create Transcoding Template Group.
    3. On the Create Transcoding Template Group page, enter a name in the Template Group Name field.
    4. In the Normal Transcoding Template section, click Add Template to create a transcoding template.
    5. In the Basic Parameters section, select hls from the Encapsulation Format drop-down list.
    6. In the Video Parameters, Audio Parameters, and Conditional Transcoding Parameters sections, configure relevant parameters as required. For more information about the meaning and limits of each parameter, see Audio and video transcoding.
    7. In the Advanced Parameters section, turn on Video Encryption and keep Alibaba Cloud Proprietary Cryptography selected for Encryption Method.
      Encryption template
      Note When you call the SubmitTranscodeJobs operation, you can use the TemplateGroupId parameter to encrypt and transcode videos based on the specified template and key.
    8. Click Save. On the Transcoding Template Groups page that is displayed, obtain and record the encryption template ID.
  2. Authorize ApsaraVideo VOD to access your KMS resources.

    To use RAM to grant ApsaraVideo VOD permissions to access your KMS resources, click Confirm Authorization Policy on the Cloud Resource Access Authorization page.

    Cloud Resource Access Authorization
  3. Build your key management service that encapsulates Alibaba Cloud KMS.
    Call the GenerateDataKey operation to generate an AES_128 key. Specify the value of KeyId as the service key and set KeySpec to AES_128. Other parameters are optional. If you do not configure the parameters based on the preceding steps, the encryption may fail.

    After the call is successful, record the value of the returned parameter CiphertextBlob, which indicates the ciphertext key.

    Note You are charged for using a key. For more information, see Fees for calling API operations.
  4. Build a token issuance service to generate MtsHlsUriToken.
    The following shows the Java sample code and the required configurations for the code.
    • ENCRYPT_KEY: a custom string for encryption. The string is 16 characters in length.
    • INIT_VECTOR: a custom string. The string is 16 characters in length and cannot contain special characters.
    • playToken.generateToken(""): a custom string. The string is 16 characters in length.

    The token generated by the final code is 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 {
        // The following parameters are not required if you do not use the AES algorithm to generate a token.
        private static String ENCRYPT_KEY = "";  // A custom string for encryption.
        private static String INIT_VECTOR = "";  // A custom string that is 16 characters in length and cannot contain special characters.
    
        public static void main(String[] args) throws Exception {
    
            PlayToken playToken = new PlayToken();
            playToken.generateToken("");
        }
    
        /**
         * Generate a token based on the configured parameters.
         * Note:
         * 1. The parameters can be the user ID (UID) and the type of the playback device.
         * 2. A token is generated when the token operation is called.
         * @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), "_");
            // Set the validity period of a token to 30 seconds. You can change the value based on your requirements.
            long expire = System.currentTimeMillis() + 30000L;
            base += "_" + expire;   // A custom string that is 16 characters in length. In this example, 2 more characters are required because the timestamp contains 13 characters and the underscore (_) is 1 character in length. You can change the value of the base parameter. Ensure that the value is 16 characters in length.
            // Generate a token.
            String token = encrypt(base, ENCRYPT_KEY);
            System.out.println(token);
            // Save the token. The validity of the token is verified during decryption, for example, the validity period and the number of times the token is used.
            saveToken(token);
            return token;
        }
        /**
         * Check whether the token is valid.
         * Note:
         * 1. Before the decryption operation returns the playback key, the decryption service checks whether the token is legitimate and valid.
         * 2. We recommend that you check the validity period of the token and the number of times the token is used.
         * @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);
            // Check the validity period of the token.
            Long expireTime = Long.valueOf(base.substring(base.lastIndexOf("_") + 1));
            if (System.currentTimeMillis() > expireTime) {
                return false;
            }
            // Obtain the token information from the database and check whether the token is valid. You can customize the logic.
            Token dbToken = getToken(token);
            // Check whether the token has been used.
            if (dbToken == null || dbToken.useCount > 0) {
                return false;
            }
            // Obtain the business attributes for verification.
            String businessInfo = base.substring(0, base.lastIndexOf("_"));
            String[] items = businessInfo.split("_");
            // Verify the validity of the business attributes. You can customize the logic.
            return validateInfo(items);
        }
        /**
         * Save the token to the database.
         * You can customize the logic.
         *
         * @param token
         */
        public void saveToken(String token) {
            // TODO. Save the token.
        }
        /**
         * Query the token.
         * You can customize the logic.
         *
         * @param token
         */
        public Token getToken(String token) {
            // TODO. Obtain the token from the database to check whether the token is valid.
            return null;
        }
        /**
         * Verify the validity of the business attributes. You can customize the logic.
         *
         * @param infos
         * @return
         */
        public boolean validateInfo(String... infos) {
            // TODO. Verify the validity of the information, such as the UID.
            return true;
        }
        /**
         * Generate a token by using the AES algorithm.
         *
         * @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);
        }
        /**
         * Decrypt the token by using the AES algorithm.
         *
         * @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);
        }
        /**
         * Obtain the token information. The sample information provided is for reference only. You can obtain more token information based on your business scenario.
         */
        class Token {
            // Obtain the number of times that the token can be used. Modifications must be synchronized in a distributed environment.
            int useCount;
            // The content of the token.
            String token;
        }}
  5. Build a decryption service.
    Notice The decryption service must be started before you play the video. Otherwise, the video cannot be decrypted as expected.

    Call the Decrypt operation of KMS to decrypt EDKs and obtain DKs. If you want to authenticate the player that requests decryption, build a token issuance service and make sure that the issued token can be parsed and verified by the decryption service.

    The GenerateDataKey operation can be called to generate a ciphertext key and a plaintext key. The plaintext key is encoded in Base64. The decryption operation returns the key after the operation decodes the key by using the Base64 algorithm.

    The following shows the Java sample code and the required configurations for the code.
    • region: Enter a region, for example, cn-shanghai for China (Shanghai).
    • AccessKey: Enter the AccessKey ID and AccessKey secret of your account.
    • httpserver: Enter the port number to start the service based on your business requirements.
    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 {
            // The region where KMS is used. The region must be the same as the region where the video resides.
            String region = "";
            // The AccessKey pair that is authorized to access KMS.
            String accessKeyId = "";
            String accessKeySecret = "";
            client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret));
        }
        /**
         * Note:
         * 1. Receive a decryption request and obtain the ciphertext key and token.
         * 2. Call the decryption operation of KMS to obtain the plaintext key.
         * 3. Decode the plaintext key by using the Base64 algorithm and return the decoded key.
         */
        public class HlsDecryptHandler implements HttpHandler {
            /**
             * Process the decryption request.
             * @param httpExchange
             * @throws IOException
             */
            public void handle(HttpExchange httpExchange) throws IOException {
                String requestMethod = httpExchange.getRequestMethod();
                if ("GET".equalsIgnoreCase(requestMethod)) {
                    // Check whether the token is valid.
                    String token = getMtsHlsUriToken(httpExchange);
                    boolean validRe = validateToken(token);
                    if (!validRe) {
                        return;
                    }
                    // Obtain the ciphertext key from the video URL.
                    String ciphertext = getCiphertext(httpExchange);
                    if (null == ciphertext)
                        return;
                    // Decrypt the key in KMS and decode the key by using the Base64 algorithm.
                    byte[] key = decrypt(ciphertext);
                    // Configure the headers.
                    setHeader(httpExchange, key);
                    // Return the key that is decoded by using the Base64 algorithm.
                    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);
            }
            /**
             * Call the decryption operation of KMS to decrypt the key and decode the plaintext key by using the Base64 algorithm.
             * @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();
                    // Note: You must decode the key by using the Base64 algorithm.
                    return Base64.decodeBase64(plaintext);
                } catch (ClientException e) {
                    e.printStackTrace();
                    return null;
                }
            }
            /**
             * Check whether the token is valid.
             * @param token
             * @return
             */
            private boolean validateToken(String token) {
                if (null == token || "".equals(token)) {
                    return false;
                }
                // TODO. You can customize the logic to check whether the token is valid.
                return true;
            }
            /**
             * Obtain the ciphertext key from the video 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;
                }
            }
            /**
             * Obtain the token parameter.
             *
             * @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;
                }
            }
        }
        /**
         * Start the service.
         *
         * @throws IOException
         */
        private void serviceBootStrap() throws IOException {
            HttpServerProvider provider = HttpServerProvider.provider();
            // You can customize the listening port. The listening port can receive up to 30 requests at the same time.
            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();
        }}
  6. Upload a video.

    Use the non-transcoding template to create the credential and the URL that you can use to upload videos. For more information about how to upload a video in the ApsaraVideo VOD console, see Upload media files. For more information about how to upload a video by calling a server operation, see CreateUploadVideo.

  7. Receive the upload callback message.
    Call the SetMessageCallback operation to configure the callback and call the GetMessageCallback operation to query the callback message. If you receive the callback message of FileUploadComplete, the video is uploaded to ApsaraVideo VOD.
  8. Start HLS encryption and transcoding.
    Call the SubmitTranscodeJobs operation to start HLS encryption and transcoding.
    The following shows the Java sample code and the required configurations for the code.
    • request.setTemplateGroupId(""): Specify the ID of the encryption template.
    • request.setVideoId(""): Specify the video ID.
    • encryptConfig.put("CipherText",""): Specify the value of the CiphertextBlob parameter that is obtained in Step 3.
    • encryptConfig.put("DecryptKeyUri",""): Specify the playback URL, the value of CiphertextBlob, and the value of MtsHlsUriToken. For example, if the video is played by using the local port 8099, the playback URL is: http://127.0.0.1:8099?CipherText=CiphertextBlob value&MtsHlsUriToken=MtsHlsUriToken value.
    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 {
    
        private static String accessKeyId = "accessKeyId";
        private static String accessKeySecret = "accessKeySecret";
    
        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";  // Specify the region where you access ApsaraVideo VOD.
            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();
            }
        }
    }       
  9. Check whether HLS encryption and transcoding are successful.

    You can log on to the ApsaraVideo VOD console to view the video URL. Use the following methods to check whether HLS encryption is successful.

    • After a video is encrypted and transcoded, if only one URL in the M3U8 format is generated for the video, transcoding for the video failed.
    • After a video is encrypted and transcoded, if a URL in the M3U8 format is generated and a file in the original format such as MP4 exists, you can determine whether HLS encryption is successful by checking whether the M3U8 format includes the Alibaba Cloud Private Encryption flag. In most cases, HLS encryption is successful if "Alibaba Cloud Private Encryption" is included.
    • If the preceding methods fail, copy the URL of the M3U8 file that contains the encryption flag and run the curl -v "URL of the M3U8 file" command to check whether the obtained content includes the key information URI="<The decryption URL that you pass when you start HLS encryption, which is the value of the DecryptKeyUri parameter in EncryptConfig>". If the key information exists, HLS encryption is successful.

Playback procedure

  1. Obtain the playback URL and credential of a video.

    Call the GetPlayInfo and GetVideoPlayAuth operations to obtain the playback URL and credential of a video.

  2. Specify the authentication information.

    After the player obtains the URL of the M3U8 file, the player parses and accesses the URI in the EXT-X-KEY tag of the M3U8 file. This way, the player obtains the URI of the decryption operation that contains the ciphertext key. This URI is the value of the DecryptKeyUri parameter in EncryptConfig that you specify when you start HLS encryption.

    If you want only authorized users to access the video, the player must provide the authentication information that you acknowledged when the player obtained the decryption key. The authentication information can be specified by using the MtsHlsUriToken parameter.

    Example:

    • If the playback URL of a video is https://demo.aliyundoc.com/encrypt-stream****-hd.m3u8, the MtsHlsUriToken parameter must be included in the request.
    • The request URL is https://demo.aliyundoc.com/encrypt-stream****-hd.m3u8?MtsHlsUriToken=<token>.
    • The decryption URL is https://demo.aliyundoc.com?Ciphertext=ZjJmZGViNzUtZWY1Mi00Y2RlLTk3MTMtOT****
    • The decryption request URL is https://demo.aliyundoc.com?Ciphertext=ZjJmZGViNzUtZWY1Mi00Y2RlLTk3MTMtOT****&MtsHlsUriToken=<issued token>.
  3. Play the video.
    After the player obtains the URI of the decryption URL, the player automatically requests the decryption operation to obtain the decryption key. After the player obtains the decryption key, the player decrypts the encrypted TS file for playback.