Overview

To improve the security of decryption keys that are used to decrypt and play videos encrypted in HTTP Live Streaming (HLS) encryption mode, you must provide both the token service and the decryption service. The token service is used to generate tokens, and the decryption service is used to verify tokens and obtain decryption keys.
Note
  • Content Delivery Network (CDN) rewrites a decryption request with a token. To use HLS encryption, you must submit a ticket to enable the feature of rewriting a decryption request with a token.
  • If the streaming URL is not a CDN URL, the token parameter is not supported.

Basic principle

To decrypt and play videos that are encrypted in HLS encryption mode, you must provide both the token service and the decryption service. All the other business logic is encapsulated in ApsaraVideo Player. The following figure shows the basic principle.
Note If you do not use ApsaraVideo Player, you must encapsulate the process of decryption and playback by yourself.
Basic principle

Preparations

To decrypt and play videos that are encrypted in HLS encryption mode, you must provide both the token service and the decryption service. The decryption service is used to obtain decryption keys and the token service is used to generate tokens.

Token service

The token service is used to generate tokens based on the user ID (UID), video ID, and terminal type such as web, iOS, and Android.

Note
  • A standard player requests for a decryption key only once to play a video. Therefore, tokens that can be used only once and have a validity period are preferred.
  • For more information about how to verify a generated token, see Sample code of the token service

Decryption service

The decryption service is used to authenticate the service requester and return a decryption key.
  • The decryption service checks whether the token is valid based on the passed token parameter. For example, the decryption service checks whether the token is generated by the token service, whether the token expires, and whether the token has been used.
  • If the token is valid, the decryption service obtains the plaintext key based on the ciphertext key. Then, the decryption service decodes the plaintext key by using the Base64 algorithm and returns the decoded key.
Note
  • In this case, the decryption service authenticates the service requester by verifying the validity of the token. The token is passed in the MtsHlsUriToken parameter to the decryption service.
  • For more information about the decryption service, see Sample code of the decryption service.

Implementation

The following section describes the process of decrypting and playing videos that are encrypted in HLS encryption mode.
Note
  • This section describes how ApsaraVideo Player decrypts and plays videos that are encrypted in HLS encryption mode.
  • If you do not use ApsaraVideo Player, you must encapsulate the process of requesting for the playback service by yourself. After you obtain the URL of an encrypted video, you can directly pass the URL to the player for decryption and playback. For more information, see GetPlayInfo.
  1. You request the token service to issue a token.
  2. The token service generates a token by using the received request information.
  3. The token service sends the generated token to you.
    Note ApsaraVideo Player does not complete the preceding steps. You must call the token service by yourself to generate a token and pass the token to the player. If you do not use ApsaraVideo Player, you can integrate the preceding steps into the playback logic of your player.
  4. You pass the token in the MtsHlsUriToken parameter that is contained in the PlayConfig parameter of the player.
  5. The player obtains the value of the MtsHlsUriToken parameter and video ID. After that, the player obtains the streaming URL from the playback service.
    Note
  6. The player obtains the URL of the M3U8 file that is used to implement HLS encryption and requests for the M3U8 file.
  7. CDN rewrites the content of the M3U8 file by adding the MtsHlsUriToken parameter to the decryption request. The following figure shows an example.CDN rewriting
  8. The player parses the content of the M3U8 file to obtain the URL of the decryption operation. Then, the player sends a request to decryption operation to obtain the decryption key. Example:

    http://demo.com/ddf56e501d07402796c468bbea08ec8c/9e712e72879b93f8933d5f9eca4bacaa-fd-encrypt-stream.m3u8?MtsHlsUriToken=NWItZGU5ZWEwODRlMzky

  9. After the decryption service receives the request, the decryption service calls the token operation to verify the validity of the token.
  10. If the token is valid, the decryption service decodes the plaintext key by using the Base64 algorithm and sends the decoded key to the player.
  11. After the player obtains the decryption key, the player decrypts and plays the video that is encrypted in HLS encryption mode.
    Note Note: Before a player plays a video, the player calls the decryption operation to obtain the decryption key only once. The player no longer calls the decryption operation during subsequent decryption and playback.

Sample code of the token service

This section provides sample code on how to generate and verify a token. You cannot use the sample code for actual deployment.
Note
  • The following code provides an example on how to generate and verify a token. You cannot use the sample code for actual deployment.
  • The sample code generates a token by using the Advanced Encryption Standard (AES) algorithm. You can also use other methods to generate a token.
  • You can customize the logic that is used to save and obtain a generated token.
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 = "";
    private static String INIT_VECTOR = "";
    /**
     * Generate a token based on the passed parameters.
     * Note:
     * 1. The parameters can be the UID and the type of the terminal that is used to play the video.
     * 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 customize the value as needed.
        long expire = System.currentTimeMillis() + 30000L;
        base += "_" + expire;
        // Generate a token.
        String token = encrypt(base, ENCRYPT_KEY);
        // 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;
    }
    /**
     * Verify the validity of the token.
     * Note:
     * 1. Before the decryption operation returns the playback key, the decryption service verifies the legitimacy and validity of the token.
     * 2. We recommend that you check the validity period of the token and 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 the validity of the token. You can customize the logic.
        TokenInfo 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 TokenInfo getToken(String token) {
        // TODO. Obtain the token from the database to verify the validity of the token.
        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, for example, whether the UID is valid.
        return true;
    }
    /**
     * Generate a token by using the AES algorithm.
     *
     * @param key
     * @param value
     * @return
     * @throws Exception
     */
    public String encrypt(String key, String value) 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 key, String encrypted) 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 is provided for reference only. You can provide more token information.
     */
    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;
    }}

Sample code of the decryption service

Note The following sample code is provided only for testing. The code can be run but cannot be used for online deployment.
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 Key Management Service (KMS) is used. The region must be the same as the region where the video resides.
        String region = "<Region of the video>";
        // The AccessKey that is used to access KMS.
        String accessKeyId = "<Your AccessKeyId>";
        String accessKeySecret = "<Your AccessKeySecrect>";
        client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret));
    }
    /**
     * Note:
     * 1. Receive a decryption request that contains 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 returns 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)) {
                // Verify the validity of the token.
                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);
                // Set the header.
                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;
            }
        }
        /**
         * Verify the validity of the token.
         * @param token
         * @return
         */
        private boolean validateToken(String token) {
            if (null == token || "".equals(token)) {
                return false;
            }
            // TODO. You can customize logic to verify the validity of the token.
            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();
        // Set a listener on port 9999, which can accept 30 requests at the same time.
        HttpServer httpserver = provider.createHttpServer(new InetSocketAddress(9999), 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();
    }}