Java tutorial
/* Copyright (c) 2014, CableLabs, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package org.cablelabs.widevine.keyreq; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.security.MessageDigest; import java.util.List; import java.util.Properties; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.IvParameterSpec; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.cablelabs.widevine.Track; public class KeyRequest { private static final String POLICY = ""; private static final String CLIENT_ID = null; private static final String[] DRM_TYPES = { "WIDEVINE" }; private static final String TEST_PROVIDER = "widevine_test"; private static final String TEST_SERVER_URL = "https://license.uat.widevine.com/cenc/getcontentkey/widevine_test"; private static final String SIGN_PROPS_URL = "url"; private static final String SIGN_PROPS_KEY = "key"; private static final String SIGN_PROPS_IV = "iv"; private static final String SIGN_PROPS_PROVIDER = "provider"; private String content_id; private List<Track> tracks; private boolean sign_request = false; private String license_url; private byte[] sign_key; private byte[] sign_iv; private String provider; private int rollingKeyStart = -1; private int rollingKeyCount = -1; /** * Creates a new key request for the given list of tracks. 1 key per track * * @param content_id the unique content ID * @param tracks the track list * @param sign_request true if the request should be signed, false otherwise * @throws IllegalArgumentException */ public KeyRequest(String content_id, List<Track> tracks) throws IllegalArgumentException { // Validate arguments if (content_id == null || content_id.isEmpty()) throw new IllegalArgumentException("Must provide a valide content ID: " + content_id); if (tracks == null || tracks.size() == 0) throw new IllegalArgumentException( "Must provide a non-empty list of tracks: " + ((tracks == null) ? "null" : "empty list")); this.content_id = content_id; this.tracks = tracks; } /** * Creates a new key request for the given list of tracks. Multiple (rolling) keys per * track * * @param content_id the unique content ID * @param tracks the track list * @param sign_request true if the request should be signed, false otherwise * @param rollingKeyStart the start time of the first key * @param rollingKeyCount the number of keys * @throws IllegalArgumentException */ public KeyRequest(String content_id, List<Track> tracks, int rollingKeyStart, int rollingKeyCount) throws IllegalArgumentException { this(content_id, tracks); if (rollingKeyCount == 0) throw new IllegalArgumentException("Must provide a non-zero rolling key count: " + rollingKeyCount); this.rollingKeyStart = rollingKeyStart; this.rollingKeyCount = rollingKeyCount; } /** * Indicates that this request should be signed and that it should use the credentials * in the given properties file * <p> * The properties file contains the following properties: * <p> * <b>url</b> : The key server URL * <b>key</b> : The 32-byte signing key, hexadecimal notation * <b>url</b> : The 16-byte initialization vector, hexadecimal notation * <b>url</b> : The provider name * * @param props_file the signing properties file * @throws IOException if there was an error reading from the properties file * @throws FileNotFoundException if the properties file was not found */ public void setSigningProperties(String props_file) throws FileNotFoundException, IOException { Properties props = new Properties(); props.load(new FileInputStream(props_file)); String prop; // Key server URL if ((prop = props.getProperty(SIGN_PROPS_URL)) == null) throw new IllegalArgumentException( "'" + SIGN_PROPS_URL + "' property not found in request signing properties file"); license_url = prop; // Signing key if ((prop = props.getProperty(SIGN_PROPS_KEY)) == null) throw new IllegalArgumentException( "'" + SIGN_PROPS_KEY + "' property not found in request signing properties file"); sign_key = Base64.decodeBase64(prop); if (sign_key.length != 32) throw new IllegalArgumentException("Request signing key is not 32 bytes in length"); // Signing initialization vector if ((prop = props.getProperty(SIGN_PROPS_IV)) == null) throw new IllegalArgumentException( "'" + SIGN_PROPS_IV + "' property not found in request signing properties file"); sign_iv = Base64.decodeBase64(prop); if (sign_iv.length != 16) throw new IllegalArgumentException("Request initialization vector is not 16 bytes in length"); // Provider name if ((prop = props.getProperty(SIGN_PROPS_PROVIDER)) == null) throw new IllegalArgumentException( "'" + SIGN_PROPS_PROVIDER + "' property not found in request signing properties file"); provider = prop; sign_request = true; } /** * Perform the key request. * * @return the response message */ public ResponseMessage requestKeys() { int i; Gson gson = new GsonBuilder().disableHtmlEscaping().create(); Gson prettyGson = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); // Create request object RequestMessage requestMessage = new RequestMessage(); requestMessage.content_id = Base64.encodeBase64String(content_id.getBytes()); requestMessage.policy = POLICY; requestMessage.client_id = CLIENT_ID; requestMessage.drm_types = DRM_TYPES; // Add the track requests to the message requestMessage.tracks = new RequestMessage.Track[tracks.size()]; i = 0; for (Track t : tracks) { RequestMessage.Track track = new RequestMessage.Track(); track.type = t.type; requestMessage.tracks[i++] = track; } // Rolling keys if (rollingKeyCount != -1 && rollingKeyStart != -1) { requestMessage.crypto_period_count = rollingKeyCount; requestMessage.first_crypto_period_index = rollingKeyStart; } // Convert request message to JSON and base64 encode String jsonRequestMessage = gson.toJson(requestMessage); System.out.println("Request Message:"); System.out.println(prettyGson.toJson(requestMessage)); // Create request JSON Request request = new Request(); request.request = Base64.encodeBase64String(jsonRequestMessage.getBytes()); String serverURL = null; if (sign_request) { // Create message signature try { MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); sha1.update(jsonRequestMessage.getBytes()); byte[] sha1_b = sha1.digest(); System.out.println("SHA-1 hash of JSON request message = 0x" + Hex.encodeHexString(sha1_b)); // Use AES/CBC/PKCS5Padding with CableLabs Key and InitVector SecretKeySpec keySpec = new SecretKeySpec(sign_key, "AES"); IvParameterSpec ivSpec = new IvParameterSpec(sign_iv); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // Encrypt the SHA-1 hash of our request message cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted = cipher.doFinal(sha1_b); System.out .println("AES/CBC/PKCS5Padding Encrypted SHA1-hash = 0x" + Hex.encodeHexString(encrypted)); request.signer = provider; request.signature = Base64.encodeBase64String(encrypted); serverURL = license_url; } catch (Exception e) { System.out.println("Error performing message encryption! Message = " + e.getMessage()); System.exit(1); } } else { request.signer = TEST_PROVIDER; serverURL = TEST_SERVER_URL; } String jsonRequest = gson.toJson(request); System.out.println("Request:"); System.out.println(prettyGson.toJson(request)); String jsonResponseStr = null; try { // Create URL connection URL url = new URL(serverURL); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); con.setDoOutput(true); System.out.println("Sending HTTP POST to " + serverURL); // Write POST data DataOutputStream out = new DataOutputStream(con.getOutputStream()); out.writeBytes(jsonRequest); out.flush(); out.close(); // Wait for response int responseCode = con.getResponseCode(); System.out.println("Received response code -- " + responseCode); // Read response data DataInputStream dis = new DataInputStream(con.getInputStream()); int bytesRead; byte responseData[] = new byte[1024]; StringBuffer sb = new StringBuffer(); while ((bytesRead = dis.read(responseData)) != -1) { sb.append(new String(responseData, 0, bytesRead)); } jsonResponseStr = sb.toString(); } catch (Exception e) { System.err.println("Error in HTTP communication! -- " + e.getMessage()); System.exit(1); } Response response = gson.fromJson(jsonResponseStr, Response.class); System.out.println("Response:"); System.out.println(prettyGson.toJson(response)); String responseMessageStr = new String(Base64.decodeBase64(response.response)); ResponseMessage responseMessage = gson.fromJson(responseMessageStr, ResponseMessage.class); System.out.println("ResponseMessage:"); System.out.println(prettyGson.toJson(responseMessage)); return responseMessage; } }