開発情報・ナレッジ

投稿者: ShiningStar株式会社 2024年9月20日 (金)

SPIRALのデータを暗号化・復号化してダウンロードさせる方法

全体像

SPIRALのAPIから取得したデータを暗号化し、ダウンロード可能な形式に変換します。
ダウンロードしたファイルは、別ページにて復号化し、CSVとしてダウンロードできます。

データを暗号化しダウンロードボタンに引き渡す設定

PHP

<?php
//------------------------------
// 設定値
//------------------------------
define("API_URL", "https://api.spiral-platform.com/v1");
define("API_KEY", "");
define("APP_ROLE", "");
define("DB_ID", "");
define("APP_ID", "");
define("ENCRYPT_KEY", "This_Is_A_32-Character_Key_Ex@mp");// 暗号化キー(文字列は32文字で設定、AES-256なので256 bits)

//------------------------------
// API実行
//------------------------------
$commonBase = CommonBase::getInstance();

$query = "?limit=200"; //最大200件まで(それ以上取得したい場合はレスポンスのnextOffsetがnullになるまで取得し続けるループ処理を加えてください)
// レコードの取得
$resultRecordListSelect = $commonBase->apiCurlAction("GET", "/apps/". APP_ID. "/dbs/". DB_ID. "/records". $query);
// JSONデータの取得
$jsonData = json_encode($resultRecordListSelect['items'], JSON_PRETTY_PRINT);

// 初期化ベクトル (IV) と暗号化タグを生成
$iv = random_bytes(16);

// データを暗号化
$encryptedData = openssl_encrypt($jsonData, 'aes-256-gcm', ENCRYPT_KEY, OPENSSL_RAW_DATA, $iv, $tag);

// IVとタグを結合してJSON形式で保存
$encryptedDataWithIvAndTag = base64_encode(json_encode([
    'iv' => base64_encode($iv),
    'tag' => base64_encode($tag),
    'data' => base64_encode($encryptedData)
]));

// 結果を保存
$SPIRAL->setTHValue("response", $encryptedDataWithIvAndTag);

//------------------------------
// 共通モジュール
//------------------------------
class CommonBase {
    /**
     * シングルトンインスタンス
     * @var UserManager
     */
    protected static $singleton;

    public function __construct() {
        if (self::$singleton) {
            throw new Exception('must be singleton');
        }
        self::$singleton = $this;
    }
    /**
     * シングルトンインスタンスを返す
     * @return UserManager
     */
    public static function getInstance() {
        if (!self::$singleton) {
            return new CommonBase();
        } else {
            return self::$singleton;
        }
    }
    /**
     * V2用 API送信ロジック
     * @return Result
     */
    function apiCurlAction($method, $addUrlPass, $data = null, $multiPart = null, $jsonDecode = null) {
        $header = array(
            "Authorization:Bearer ". API_KEY,
            "X-Spiral-Api-Version: 1.1",
        );
        if($multiPart) {
            $header = array_merge($header, array($multiPart));
        } else {
            $header = array_merge($header, array("Content-Type:application/json"));
        }
        if(APP_ROLE){
			$header = array_merge($header, array("X-Spiral-App-Role: ".APP_ROLE));
		}
        // curl
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_URL, API_URL. $addUrlPass);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
        if ($method == "POST") {
            if ($multiPart) {
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
            } else {
                curl_setopt($curl, CURLOPT_POSTFIELDS , json_encode($data));
            }
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        if ($method == "PATCH") {
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        if ($method == "DELETE") {
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        $response = curl_exec($curl);
        if (curl_errno($curl)) echo curl_error($curl);
        curl_close($curl);
        if($jsonDecode){
			return $response;
		}else{
            return json_decode($response, true);
		}
    }
}
?> 

  • API_KEY、APP_ID、DB_ID、ENCRYPT_KEYを適切な値に置き換えてください。
  • ENCRYPT_KEYは英字(大文字、小文字区別有)+数字+記号の32文字の文字列で設定してください。


認証エリア内に作成いただくことで、ユーザごとにランダムを設定することができます。
ユーザごとの暗号化キーを生成する必要があるので、DBトリガのレコードアクションを利用して設定を行ってください。

テキストフィールドに関数設定で RANDOM_NUM_ALP_SYM(32)を記載いただくことで、英数字記号の32文字のランダム値が生成されます。

PHP

$key = $SPIRAL->getAuthRecordByFieldId("3"); //暗号化キーの格納フィールドのフィールドID
define("ENCRYPT_KEY", $key);// 暗号化キー(文字列は32文字で設定、AES-256なので256 bits) 

認証エリア内にページを作成する場合は設定値のキーの部分を上記のように変更してください。

暗号化されたデータのダウンロード

上記のPHPコードでは、
$SPIRAL->setTHValue("response", $encryptedDataWithIvAndTag);
の箇所で、
暗号化されたデータをThymeleafの変数responseに引き渡しています。

以下のようなJavaScriptコードを記述することで、暗号化されたデータ(txt形式)のダウンロードボタン設置とダウンロード処理を作成できます。
HTMLとJavaScriptをそれぞれのタブへ貼り付けてください。

HTML

<button id="download-btn">ダウンロード</button>

<th:block th:if="${cp.result.isSuccess}">
    <!-- PHP正常完了した場合に表示 -->
    <p id="response" style="display:none;" th:text="${cp.result.value['response']}"></p>
</th:block>
<th:block th:if="${!cp.result.isSuccess}">
    <!-- PHPにエラー起こった場合に表示 -->
    <p th:text="${cp.result.errorMessage}">error message</p>
</th:block>


JavaScript
  document.addEventListener('DOMContentLoaded', () => {
    // 設定値
    const textFileName = 'data.txt'; // ダウンロードされる暗号化されたテキスト名

    const responseElement = document.getElementById('response');
    const encryptedDataWithIvAndTag = responseElement.textContent;

    document.getElementById('download-btn').addEventListener('click', () => {
        try {
            const blob = new Blob([encryptedDataWithIvAndTag], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = url;
            a.download = textFileName;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);

            URL.revokeObjectURL(url);
        } catch (error) {
            console.error('ファイルダウンロードエラー:', error);
            alert('ファイルのダウンロード中にエラーが発生しました。');
        }
    });
  });

  • ダウンロードボタンを押下すると、暗号化されたデータがdata.txtという名前でダウンロードされます。
  • ダウンロードが確認出来れば暗号化ファイルダウンロードの設定が完了になります。

復号化

ダウンロードしたファイルを復号化し、CSV形式に変換してダウンロードさせます。

HTML

<label for="key">復号化キー (32文字):</label>
<input type="text" id="key" name="key" required><br><br>

<label for="file">暗号化されたファイルを選択:</label>
<input type="file" id="file" name="file" accept=".txt" required><br><br>

<button id="decrypt-btn">CSVとしてダウンロード</button>

/*認証エリア内に設置する場合以下の様に認証レコード値を出力
<label for="key">暗号化キー:</label>
<span th:text="${siteClient.record[3]}">Example</span><br><br>
*/
JavaScript
  document.addEventListener('DOMContentLoaded', () => {
    // 設定値
    const csvFileName = 'data.csv';  // ダウンロードされる復号化されたCSVファイル名

    async function decryptData(encryptedData, key, iv, tag) {
      const keyBuffer = new TextEncoder().encode(key);
      const cryptoKey = await crypto.subtle.importKey(
        'raw',
        keyBuffer,
        { name: 'AES-GCM' },
        false,
        ['decrypt']
      );

      const decodedEncryptedData = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
      const decodedIV = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
      const decodedTag = Uint8Array.from(atob(tag), c => c.charCodeAt(0));
      
      const dataWithTag = new Uint8Array(decodedEncryptedData.length + decodedTag.length);
      dataWithTag.set(decodedEncryptedData);
      dataWithTag.set(decodedTag, decodedEncryptedData.length);

      const decryptedArrayBuffer = await crypto.subtle.decrypt(
        {
          name: 'AES-GCM',
          iv: decodedIV,
          additionalData: new Uint8Array(),
          tagLength: 128
        },
        cryptoKey,
        dataWithTag
      );

      const decryptedText = new TextDecoder().decode(decryptedArrayBuffer);
      return decryptedText;
    }

    document.getElementById('decrypt-btn').addEventListener('click', async () => {
      const fileInput = document.getElementById('file');
      const key = document.getElementById('key').value;

      if (key.length !== 32) {
        alert('復号化キーは32文字である必要があります。');
        return;
      }

      const file = fileInput.files[0];
      if (!file) {
        alert('ファイルを選択してください。');
        return;
      }

      const reader = new FileReader();
      reader.onload = async (e) => {
        const encryptedDataWithIvAndTag = e.target.result;

        try {
          const encryptedObject = JSON.parse(atob(encryptedDataWithIvAndTag));
          const iv = encryptedObject.iv;
          const tag = encryptedObject.tag;
          const encryptedData = encryptedObject.data;

          const decryptedData = await decryptData(encryptedData, key, iv, tag);
          const jsonData = JSON.parse(decryptedData);
          const csv = convertToCSV(jsonData);

          const blob = new Blob([csv], { type: 'text/csv' });
          const url = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = url;
          a.download = csvFileName;
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
          URL.revokeObjectURL(url);
        } catch (error) {
          console.error('復号化エラー:', error);
          alert('復号化中にエラーが発生しました。');
        }
      };

      reader.readAsText(file);
    });

    function flattenObject(ob) {
      let toReturn = {};

      for (let i in ob) {
        if (!ob.hasOwnProperty(i)) continue;

        if (typeof ob[i] === 'object' && ob[i] !== null) {
          let flatObject = flattenObject(ob[i]);
          for (let x in flatObject) {
            if (!flatObject.hasOwnProperty(x)) continue;
            toReturn[i + '.' + x] = flatObject[x];
          }
        } else {
          toReturn[i] = ob[i];
        }
      }
      return toReturn;
    }

    function convertToCSV(objArray) {
      const array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray;
      const flattenedArray = array.map(item => flattenObject(item));
      let str = '';
      let headers = Object.keys(flattenedArray[0]).join(',') + '\r\n';
      str += headers;

      for (const obj of flattenedArray) {
        let line = '';
        for (const index in obj) {
          if (line !== '') line += ',';
          line += obj[index];
        }
        str += line + '\r\n';
      }

      return str;
    }
  });

  • 必要項目を入力の上、「CSVとしてダウンロード」ボタンを押下すると、ファイル選択ダイアログが表示され、
  • 復号化されたデータがdata.csvという名前でダウンロードされれば、設定が完了になります。

注意点

  • 本記事のコードはサンプルです。
  • 実際の運用環境に合わせて適切に修正・確認をお願いします。
  • セキュリティに関する情報は常に最新の状態に保ち、適切な対策をお願いします。
解決しない場合はこちら コンテンツに関しての
要望はこちら