Wowza Streaming EngineのSecureTokenで利用するハッシュの生成方法

# Wowzaの視聴制限について

Wowzaというストリーミングサーバーには視聴用URLに
・視聴用URLの改変を確認するハッシュ(hash)
・視聴用URLが有効になる(視聴できるようになる)日時(starttime)
・視聴用URLが無効になる(視聴できなくなる)日時(endtime)
が付与でき、Wowza側でそれらの情報を元にした視聴継続判定を行うことができます。

この機能が適用されるのは視聴時(play)のみのため、配信(publish)側に影響はありません。(配信時のURLを変更する必要がなく継続して利用できます)

# Wowza のサイト

具体的な利用方法については以下のサイトに記載されています。
またハッシュの生成についてはページ中ほどの「Hash generation」にて説明されています。

Use Wowza SecureToken to protect streams
https://www.wowza.com/docs/how-to-protect-streaming-using-securetoken-in-wowza-streaming-engine#hls-example

# 本記事の目的

上記Wowzaのサイトには「Hash generation」にて
・どのような文字列からハッシュを生成するか
・生成したハッシュをどう視聴URLに組み込むか
は記載されていますが、具体的な生成方法が記載されていません。

そこでその具体的な生成方法を調べ始めたのですが、時間を要してしまったのでサンプルコード・サンプルコマンドと共に紹介しようと思います。

なおサンプルコードでは「RTSP example」にある情報を元にサイトに記載されているハッシュと同じものが生成できるか確認しています。

# サンプルコード ― JavaScript編

```JavaScript:wowza_secure_token_sample.js
const crypto = require("crypto");

var clientIP = null;
var host = "10.0.2.31";
var url= "rtsp://"+ host + ":1935/";
var stream = "vod/_myInstance_/sample.mp4";
// var start = new Date().getTime();
// var end = new Date().getTime() + (10 * 60);
var end = 1500000000;
var secret = "xyzSharedSecret";                                       // Shared Secret
var tokenName = "wowzatoken";                                          // Hash Query Parameter Prefix

var queryparam = [];
// queryparam.push(tokenName + "starttime=" + start);
queryparam.push(tokenName + "endtime=" + end);
queryparam.push(tokenName + "CustomParameter=abcdef");

var hashparam = [];
if(clientIP){
    hashparam.push(clientIP);
}
hashparam.push(secret);

hashparam = hashparam.concat(queryparam);

hashparam.sort();                                                      // ハッシュ生成用のパラメータをアルファベット順に

var strUsedHashing = stream + "?";
for (var paramIndex in hashparam) {
    strUsedHashing += hashparam[paramIndex] + "&";
}

strUsedHashing = strUsedHashing.replace(/&$/, "");

console.log("String used for hashing =", strUsedHashing);

var URL = url + stream;

if(url.match("/(http)/")) {
    URL = URL + "/playlist.m3u8";
}

URL += "?";

var query = "";
for (paramIndex in queryparam){
    query += queryparam[paramIndex] + "&";
}

var hash = crypto.createHash('sha256').update(strUsedHashing).digest('hex'); // hash binary(true:binary, false:hex string)
console.log("hash(hex string) =", hash);

var base64str = Buffer.from(hash, 'hex').toString("base64");           // hash binaryをbase64化
base64str = base64str.replace("+", "-");                               // safe base 64
base64str = base64str.replace("\/", "_");                              // safe base 64

URL += query + tokenName + "hash=" + base64str;

console.log(URL);
```

```plantext:実行結果
$ node wowza_secure_token_sample.js
String used for hashing = vod/_myInstance_/sample.mp4?wowzatokenCustomParameter=abcdef&wowzatokenendtime=1500000000&xyzSharedSecret
hash(hex string) = 909e7dd71076953f97d0e03d51da11c7ad6ec29e80fc8a1273f8c2c7ff61d65f
rtsp://10.0.2.31:1935/vod/_myInstance_/sample.mp4?wowzatokenendtime=1500000000&wowzatokenCustomParameter=abcdef&wowzatokenhash=kJ591xB2lT-X0OA9UdoRx61uwp6A_IoSc/jCx/9h1l8=
```


# サンプルコード ― PHP編

```PHP:wowza_secure_token_sample.php
<?php

$clientIP = null;
$host = "10.0.2.31";
$url= "rtsp://".$host.":1935/";
$stream = "vod/_myInstance_/sample.mp4";
// $start = time();
$end = 1500000000;//strtotime("+10 minutes");
$secret = "xyzSharedSecret";                                       // Shared Secret
$tokenName = "wowzatoken";                                          // Hash Query Parameter Prefix

if(!is_null($clientIP)){
    $hashparam[] = $clientIP;
}
$hashparam[] = $secret;
$queryparam = array();
// $queryparam[] = $hashparam[] = "{$tokenName}starttime=".$start;
$queryparam[] = $hashparam[] = "{$tokenName}endtime=".$end;
$queryparam[] = $hashparam[] = "{$tokenName}CustomParameter=abcdef";

sort($hashparam);                                                   // ハッシュ生成用のパラメータをアルファベット順に

$strUsedHashing = $stream."?";
foreach($hashparam as $entry){
    $strUsedHashing.= $entry."&";
}
$strUsedHashing = preg_replace("/(\&)$/","", $strUsedHashing);

print "String used for hashing = ". $strUsedHashing . "\n";

$URL = $url.$stream;
if(preg_match("/(http)/",$url)){
    $URL = $URL."/playlist.m3u8";
}
$URL .= "?";

$query = "";
foreach($queryparam as $entry){
    $query.= $entry."&";
}

$hash = hash('sha256', $strUsedHashing, false);                      // hash binary(true:binary, false:hex string)
print "hash hex string = ". $hash . "\n";

$hash = hash('sha256', $strUsedHashing, true);                      // hash binary(true:binary, false:hex string)

$base64str = base64_encode($hash);                                  // hash binaryをbase64化
$base64str = strtr($base64str, '+/', '-_');                         // safe base 64

$URL .= $query."{$tokenName}hash=".$base64str;

print $URL . "\n";

?>
```

```plantext:実行結果
$ php wowza_secure_token_sample.php
String used for hashing = vod/_myInstance_/sample.mp4?wowzatokenCustomParameter=abcdef&wowzatokenendtime=1500000000&xyzSharedSecret
hash hex string = 909e7dd71076953f97d0e03d51da11c7ad6ec29e80fc8a1273f8c2c7ff61d65f
rtsp://10.0.2.31:1935/vod/_myInstance_/sample.mp4?wowzatokenendtime=1500000000&wowzatokenCustomParameter=abcdef&wowzatokenhash=kJ591xB2lT-X0OA9UdoRx61uwp6A_IoSc_jCx_9h1l8=
```

# サンプルコマンド

```plantext:コマンドと実行結果
$ echo -n "vod/_myInstance_/sample.mp4?wowzatokenCustomParameter=abcdef&wowzatokenendtime=1500000000&xyzSharedSecret" | openssl dgst -sha256 -binary | base64
kJ591xB2lT+X0OA9UdoRx61uwp6A/IoSc/jCx/9h1l8=
```

いつもはsha256sumを利用していますが、バイナリ(バイト配列)で出力する方法が分からなかったのでopensslのdgstを利用しています。


# コードの説明

JavaScriptもPHPもほぼ同じようなコードですので、ここではJavaScriptのコードで説明しようと思います。

## パラメータ構築

```plantext:パラメータ構築
var queryparam = [];
// queryparam.push(tokenName + "starttime=" + start);
queryparam.push(tokenName + "endtime=" + end);
queryparam.push(tokenName + "CustomParameter=abcdef");

var hashparam = [];
if(clientIP){
    hashparam.push(clientIP);
}
hashparam.push(secret);

hashparam = hashparam.concat(queryparam);
```

ハッシュを生成する文字列とURLに使用するクエリ文字列が異なる(Shared Secretの有無、場合によってはクライアントIPの有無)ので、queryparam と hashparam に分けて構築しています。

## ソート

```plantext:ソート
hashparam.sort();                      // ハッシュ生成用のパラメータをアルファベット順に
```

各パラメータを&で連結した文字列を元にハッシュを生成します。
Wowzaのサイトでも説明されていますが、各パラメータはアルファベット順でソートしておく必要があります。

どこかのサイトでサンプルコードを貼られている方がいましたが、Shared Secret(secret変数)が文字列の末尾に固定されていました。今回のサンプルでは「Shared Secret」が"xyzSharedSecret"で、「Hash Query Parameter Prefix」が"wowzatoken"なので結果的に末尾で問題ありませんが、本来は「Shared Secret」も含めソートする必要があります。

Wowzaの管理画面には「Generate SecureToken Shared Secret」で「Shared Secret」を自動生成してくれる機能がありますが、この時生成したShared Secretの先頭が数字になる場合があります。この場合はsecret変数は先頭(*1)になります。

*1：クライアントIPを指定する場合は"192.168.x.x"のような文字列を指定することになります。(パラメータの先頭が"1")。Shared Secretの先頭が0、1、2で順番が変わる点はご留意ください。(パラメータの先頭が"0", "1", "2"。"1"の場合は2文字目以降も要考慮)

そしてハッシュ生成用の文字列として構築した"String used for hashing ="は、Wowzaのサイトにある"String used for hashing (in required alphabetical order):"と一緒ですね。

## ハッシュ生成

```plantext:ハッシュ生成
var hash = crypto.createHash('sha256').update(strUsedHashing).digest('hex'); // hash binary(true:binary, false:hex string)
console.log("hash(hex string) =", hash);

var base64str = Buffer.from(hash, 'hex').toString("base64");           // hash binaryをbase64化
base64str = base64str.replace("+", "-");                               // safe base 64
base64str = base64str.replace("\/", "_");                              // safe base 64
```

通常、SHAハッシュといえば、

```
909e7dd71076953f97d0e03d51da11c7ad6ec29e80fc8a1273f8c2c7ff61d65f
```

のような16進文字列を思い浮かべるのではないでしょうか？
実際私も↑をBase64化するものと思っていました。

```
$ echo -n "909e7dd71076953f97d0e03d51da11c7ad6ec29e80fc8a1273f8c2c7ff61d65f" | base64 -w0
OTA5ZTdkZDcxMDc2OTUzZjk3ZDBlMDNkNTFkYTExYzdhZDZlYzI5ZTgwZmM4YTEyNzNmOGMyYzdmZjYxZDY1Zg==
```

ですが、Base64化してみると長さが異なります。
Base64は元のデータ長より長くなることはるが短くなることはない＝＝使っている元データが異なっている、と言えます。

そこで改めてWowzaのサイトを眺めます。すると、、、

```
The following examples use SHA-256, in binary, to calculate the hash.
```

を見つけます。

そこでhash変数に入っている16進文字列をバイナリ化(バイト配列化)してからBase64を算出しています。

さらに、

```
hash – (Required) The hash generated at the client as a URL-safe Base64-encoded string. URL-safe Base64 encoding replaces the '+' character with the '-' character and the '/' character with the '_' character. For example:
```

ともありますので、Base64文字列の中に含まれる"+"を"-"に、"/"を"_"に置換します。

これでようやく視聴URLに連結するハッシュが出来上がります。

上記実行結果のURLにある"wowzatokenhash=kJ591xB2lT-X0OA9UdoRx61uwp6A_IoSc_jCx_9h1l8="がWowzaサイトのものと一致することが確認できます。


# ありがちな失敗？？？

```
$ cat work.txt
vod/_myInstance_/sample.mp4?wowzatokenCustomParameter=abcdef&wowzatokenendtime=1500000000&xyzSharedSecret
$ cat work.txt | openssl dgst -sha256 -binary | base64
uCp8OI/WUtgjWB5A76Qo2nGuL9R8TZz2RKabDeWX8Jk=
```

上記のようなこともしてしまっていました。
上記のサンプルコマンドではecho -nと改行を意識できていたのに、、、

```
$ cat work.txt | tr -d '\r' | tr -d '\n' | openssl dgst -sha256 -binary | base64
kJ591xB2lT+X0OA9UdoRx61uwp6A/IoSc/jCx/9h1l8=
```

正解は↑ですね。