Node.js で Azure BLOB オブジェクト を 30秒間 だけ Read 可能な SAS トークン を ライブラリを使わないで生成する

## tl;dr

SAS トークンの構造を学習するために、ライブラリを使わないで、Node.js で生成しました。
( Java 版は [こちら](https://qiita.com/exabugs/items/9c2acf7a78adfaeea9c5) )

公式ドキュメント : [サービス SAS を作成する](https://docs.microsoft.com/ja-jp/rest/api/storageservices/create-service-sas
)

## コード

BLOB オブジェクト を 30秒 だけ Read 可能なURL 

```javascript:main.js
const SAS = require("./SAS");

const name = "ストレージアカウント名";
const key = "キー";
const container = "コンテナ名";
const blob = "ブロブ名";
const expiry = 30; // 30 秒間有効
const filename = "ファイル名";

const sas = new SAS(name, key);
const url = sas.getUrl(container, blob, expiry, filename);
console.log(url);
```

```javascript:SAS.js
const crypto = require("crypto");

class SAS {
    // コンストラクタ
    // @param name ストレージアカウント名
    // @param key ストレージアカウントキー
    constructor(name, key) {
        this.account_name = name;
        this.account_key = key;
    }

    // トークン付きURLを生成する
    // @param container コンテナ名
    // @param blob ブロブ名
    // @param expiry_second トークン有効期間
    // @param filename ファイル名
    getUrl(container, blob, expiry_second, filename) {
        const token = this.getToken(container, blob, expiry_second, filename);
        const url = `https://${this.account_name}.blob.core.windows.net/${container}/${blob}`;
        return `${url}?${token}`;
    }

    // トークンを生成する
    // @param container コンテナ名
    // @param blob ブロブ名
    // @param expiry_second トークン有効期間
    // @param filename ファイル名
    getToken(container, blob, expiry_second, filename) {
        // パラメータ
        const now = new Date();
        const start = ""; // this.isoDate(now, start_second);
        const expiry = this.isoDate(now, expiry_second);

        const version = "2018-11-09";
        const resource = "b";
        const permission = "r";
        const ip = ""; // "0.0.0.0/0";
        const protocol = ""; // "https";

        const canonicalizedResource = `/blob/${this.account_name}/${container}/${blob}`;

        const rscc = "";
        const rscd = `attachment; filename="${filename}"`;
        const rsce = "";
        const rscl = "";
        const rsct = "";

        const identifier = "";
        const snapshot = "";

        // シグネチャ (順番は厳守)
        const stringToSign = [
            permission,
            start,
            expiry,
            canonicalizedResource,
            identifier,
            ip,
            protocol,
            version,
            resource,
            snapshot,
            rscc, // Cache-Control
            rscd, // Content-Disposition
            rsce, // Content-Encoding
            rscl, // Content-Language
            rsct, // Content-Type
        ].join("\n");

        // HMAC256
        const signature = this.getHMAC256(stringToSign);

        // クエリパラメータ (順番は任意)
        const sas = [
            ["sp", permission], //
            ["sr", resource],
            // ["st", start],
            ["se", expiry],
            // ["sip", ip],
            // ["spr", protocol],
            ["sv", version],
            ["sig", signature],
            ["rscd", rscd],
        ]
            .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
            .join("&");

        return sas;
    }

    isoDate(now, second = 0) {
        const date = new Date(now.getTime() + second * 1000);
        return date.toISOString().substring(0, 19) + "Z";
    }

    getHMAC256(input) {
        // 1. キー は Buffer (Base64 で デコード)
        const key = Buffer.from(this.account_key, "base64");
        const hmac = crypto.createHmac("sha256", key);
        // 2. 入力 は Buffer (UTF8 で デコード)
        const inp = Buffer.from(input, "utf8");
        // 3. 出力 は Buffer を Base64 で エンコード
        const out = hmac.update(inp).digest();
        return out.toString("base64");
    }

    // 上を簡単に書き直すと
    getHMAC256_(input) {
        const key = Buffer.from(this.account_key, "base64");
        return crypto.createHmac("sha256", key).update(input).digest("base64");
    }
}

module.exports = SAS;
```

### 01/29 追記

ちなみに、az コマンドで同じことをやるには、こんな感じ

```
ACCOUNT_NAME='ストレージアカウント名'
ACCOUNT_KEY='キー'
FUTURE_DATE=$(date -v+30M '+%Y-%m-%dT%H:%MZ')

az storage account generate-sas \
  --permissions r \
  --resource-types o \
  --services b \
  --expiry $FUTURE_DATE \
  --account-name $ACCOUNT_NAME \
  --account-key $ACCOUNT_KEY
```