2017年10月27日 星期五

MySQL 使用 AES 加解密資料、PHP 產生相容 MySQL 的加解密效果

MySQL 使用 AES_ENCRYPT()、AES_DECRYPT() 兩個函式,可將資料進行 AES 的加解、解密。
所以若資料庫中,有某些欄位是隱私資料,想加密後再儲存到資料表中。只要 INSERT 到資料表時用 AES_ENCRYPT() 將資料先加密;SELECT 時用 AES_DECRYPT() 將資料解密還原即可。
  • AES_ENCRYPT() 加密語法為 AES_ENCRYPT(str,key_str[,init_vector])
    str:原始字串
    key_str:自行設定的密鑰
    init_vector:此參數 MySQL 5.6.17 之後才有。且多了 block_encryption_mode 環境變數,可設定不同演算法。測試環境為 MariaDB 5.5,演算法是 ECB,沒用到此參數,後面都以 AES-128-ECB 演算法加解密測試。
  • AES_DECRYPT() 解密語法為 AES_DECRYPT(crypt_str,key_str[,init_vector])
    crypt_str:加密後的二進位資料
    key_str:自行設定的密鑰
    init_vector:ECB 演算法沒用到此參數
    解密失敗會回傳 null,也可能回傳非 null 的垃圾資料。
    「If AES_DECRYPT() detects invalid data or incorrect padding, it returns NULL. However, it is possible for AES_DECRYPT() to return a non-NULL value (possibly garbage) if the input data or the key is invalid.」
  • 密鑰長度:AES-128-ECB 密鑰長度為 128bits (As of MySQL 5.6.17, key lengths of 196 or 256 bits can be used),但使用 AES_ENCRYPT()、AES_DECRYPT() 時,如果輸入太短或太長的密鑰,MySQL 會自動處理成 128bits
  • 原始字串 str、加密後資料 crypt_str 可以是任意長度。
    而 AES-128-ECB 演算法(128bits),原資料須為 16bytes 的倍數,所以原始字串 str 加密前,AES_ENCRYPT() 會自動將長度填充為AES加密演算法須要的區塊倍數長度(16byte的倍數),解密時 AES_DECRYPT() 再將填充的字元移除。
    至於用來填充的字元,則是用原始字串 str 須要再補多少長度才會是的16byte倍數的char值(所補長度數字取char得到的字元、ASCII對應的字元),且若原字串長度剛好為 16bytes 倍數時,也會再填充一個完整的 16bytes 區塊。如此反解後,只須由最後一個字元,即可知反解後的字串最後面多少長度是填充的,才能將填充後的字串去除,得到原始字串 str。
  • 將加密完的密文,儲存到資料表,所以須知道 AES_ENCRYPT() 回傳的資料型態、資料長度。
    資料型態:加密後的資料為二進位資料。(若 str、key_str 有任一個為 null,AES_ENCRYPT 將回傳 null)
    資料長度:資料加密後的長度計算方式「16 * (trunc(string_length / 16) + 1)」,其中 trunc() 是虛擬程式碼(pseudo code),表示小數部分無條件捨去。
  • 測試加密前後資料長度:
    1. 將"ABC"資料,用"testkey"當作密鑰加密,前後的長度變化
      SELECT LENGTH("ABC"); //3 bytes
      SELECT LENGTH(AES_ENCRYPT("ABC","testkey")); //16 bytes
      SELECT LENGTH(AES_ENCRYPT("ABC","testkey123456790")); //16 bytes
      
      加密後的長度為 16*(trunc(3/16)+1)=16 bytes
    2. 將"1234567890ABCDEF"資料,用"testkey"當作密鑰加密,前後的長度變化
      SELECT LENGTH("1234567890ABCDEF"); //16
      SELECT LENGTH(AES_ENCRYPT("1234567890ABCDEF","testkey")); //32 bytes
      
      加密後的長度為 16*(trunc(16/16)+1)=32 bytes
    3. 將"1234567890ABCDEFG"資料,用"testkey"當作密鑰加密,前後的長度變化
      SELECT LENGTH("1234567890ABCDEFG"); //17
      SELECT LENGTH(AES_ENCRYPT("1234567890ABCDEFG","testkey")); //32 bytes
      
      加密後的長度為 16*(trunc(17/16)+1)=32 bytes
    4. 將"一二三四五六七八九十一二三四五六"資料,用"testkey"當作密鑰加密,前後的長度變化
      SELECT LENGTH("一二三四五六七八九十一二三四五六"); //48
      SELECT LENGTH(AES_ENCRYPT("一二三四五六七八九十一二三四五六","testkey")); //64
      SELECT c, LENGTH(c ), CHAR_LENGTH(c), LENGTH(AES_ENCRYPT(c, "testkey")) FROM zz;
      
      
      mysql> SELECT c, LENGTH(c), CHAR_LENGTH(c), LENGTH(AES_ENCRYPT(c, "testkey")) FROM test;
      
      +-------------------------------+------------+----------------+-----------------------------------+ | c | LENGTH(c ) | CHAR_LENGTH(c) | LENGTH(AES_ENCRYPT(c, "testkey")) | +-------------------------------+------------+----------------+-----------------------------------+ | 一二三四五六七八九十一二三四五六 | 48 | 16 | 64 | +-------------------------------+------------+----------------+-----------------------------------+
      可發現UTF8中,常用的中文,一個字是3 bytes,所以16個中文字,是16*3=48 bytes
      加密後的長度為 16*(trunc((16*3)/16)+1)=64 bytes
      所以假設原本 varchar(16),要改用 varbinary 儲存加密後的結果,至少須設為 varbinary(64), 若只設 varbinary(63) 或 varbinary(16)、varbinary(32),長度都不夠,加密資料無法全部儲存,將無法正確反解。
  • 加解密寫入資料表測試:
    (PHP、MySQL,用的 PHPMyAdmin 版本,常用手動改回不用16進位顯示2進位,所以直接用PHP測試)
    //$db PDO物件
    $stmt = $db->query("CREATE TABLE IF NOT EXISTS `zz` (`id` int(11) NOT NULL, `c` varchar(255) NOT NULL, `c_aes` varbinary(2) DEFAULT '', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8");
    $stmt = $db->query("ALTER TABLE zz CHANGE c_aes c_aes VARBINARY(64) NULL DEFAULT ''"); //可修改不同 VARBINARY 長度測試
    $stmt = $db->query("INSERT INTO zz (id, c, c_aes) VALUES (1, '一二三四五六七八九十一二三四五六', '') ON DUPLICATE KEY UPDATE c=VALUES(c)"); //可修改不同加密內容'一二三四五六七八九十一二三四五六'測試
    $stmt = $db->query("UPDATE zz SET c_aes = AES_ENCRYPT( c, 'testkey')");
    $stmt = $db->query("SELECT c, c_aes, LENGTH(c), CHAR_LENGTH(c), LENGTH(AES_ENCRYPT(c, 'testkey')) , AES_DECRYPT(AES_ENCRYPT(c,'testkey'),'testkey'), LENGTH(c_aes), AES_DECRYPT(c_aes,'testkey'), LENGTH(AES_DECRYPT(c_aes, 'testkey')) FROM zz");
    var_dump($stmt->fetch(\PDO::FETCH_ASSOC));
    array(9) {
      ["c"]=> //加密前字串
      string(48) "一二三四五六七八九十一二三四五六"
      ["c_aes"]=> //加密後二進位資料
      string(64) "? �Lmy; M� i� 3�zf@��r����p�'ƒ ا� ��x � � �/���r�%O� *e�:�y"
      ["LENGTH(c)"]=> //加密前byte長度
      string(2) "48"
      ["CHAR_LENGTH(c)"]=> //加密前字數
      string(2) "16"
      ["LENGTH(AES_ENCRYPT(c, 'testkey'))"]=> //加密後byte長度
      string(2) "64"
      ["AES_DECRYPT(AES_ENCRYPT(c,'testkey'),'testkey')"]=> //解密後字串
      string(48) "一二三四五六七八九十一二三四五六"
      ["LENGTH(c_aes)"]=> //加密後儲存到資料表的的二進位資料byte長度(VARBINARY太短時,可觀察到被截斷)
      string(2) "64"
      ["AES_DECRYPT(c_aes,'testkey')"]=> //將資料表儲存的加密二進位資料解密後的字串(若儲存後已被截斷,會無法正常解密)
      string(48) "一二三四五六七八九十一二三四五六"
      ["LENGTH(AES_DECRYPT(c_aes, 'testkey'))"]=> //資料表儲存的加密二進位資料解密後的字串byte長度
      string(2) "48"
    }
    
  • PHP 可以用 openssl_*、mcrypt_* 兩種方法進行 AES 加解密,但 mcrypt PHP 7.1 之後已不建議使用。以下是分別使用 openssl、mcrypt 產生跟 MySQL 相同的加解密結果。
    1. openssl,主要須處理密鑰 key 超過 16 bytes 的部分,MySQL 會對過長部分進行 XOR 運算,PHP 測試結果似乎是將過長部分截斷。
      /**
       * 模擬 MySQL AES_ENCRYPT()、AES_ENCRYPT()
       */
      class AesMySQL {
      
          /**
           * 原始的密鑰字串
           * @var string 
           */
          private $key_str;
      
          /**
           * 處理後符合規則的密鑰字串
           * @var string 
           */
          private $key;
      
          /**
           * 將原始的密鑰字串,處理成 MySQL AES_ENCRYPT() 使用的密鑰格式
           * @param string $key_str 原始的密鑰字串
           * @return string
           */
          private function getAesKey($key_str) {
              if (isset($this->key_str) && $key_str === $this->key_str) {
                  //此原始的密鑰字串已處理過
              } else {
                  //PHP:測試超過16bytes的部分似乎會截斷。
                  //MySQL:超過16bytes,依序每16bytes分成一組,每一組同位置的位元組進行XOR運算,最終處理成只有16bytes
                  //若原始長度小於16bytes,PHP、MySQL都是在後面用 chr(0) 補齊,chr(0)即"\0"
                  $key_len = 16; //處理成16bytes
                  $key_str_len = strlen($key_str);
                  if ($key_str_len <= $key_len) {
                      $pad = $key_len - $key_str_len;
                      $key = $key_str . str_repeat("\0", $pad); //"\0" 可用 chr(0) 替代
                  } else {
                      $key = substr($key_str, 0, $key_len);
                      for ($i = $key_len; $i < $key_str_len; $i++) {
                          $pos = $i % $key_len;
                          $key[$pos] = $key[$pos] ^ $key_str[$i];
                      }
                  }
                  $this->key_str = $key_str;
                  $this->key = $key;
              }
      
              return $this->key;
          }
      
          /**
           * 模擬 MySQL AES_ENCRYPT() 加密結果 (使用openssl_encrypt)
           * @param string $str
           * @param string $key_str 原始的密鑰字串
           * @return binary|null
           */
          public function aesEncrypt($str, $key_str) {
              if (null === $str || null === $key_str) {
                  return null;
              }
              //openssl_get_cipher_methods()可取得可用的演算法列表
              $cipher = "AES-128-ECB"; //MySQL使用 128bit ECB 演算法
              $key = $this->getAESKey($key_str); //密鑰用MySQL的規則再處理過(測試原本PHP太長超過16bytes的部分會截斷)
              $options = OPENSSL_RAW_DATA; //OPENSSL_RAW_DATA、OPENSSL_ZERO_PADDING
              //OPENSSL_RAW_DATA 會自動使用 PKCS#7 格式填充,所以加解密不須自己處理填充問題
              //OPENSSL_ZERO_PADDING 須自己處理填充(加密前自行加上填充、解密後自行去除填充),且回傳格式為 Base64
              //http://php.net/manual/en/function.openssl-encrypt.php#117208
              $ciphertext_raw = openssl_encrypt($str, $cipher, $key, $options); //ECB沒使用iv
              return $ciphertext_raw;
          }
      
          /**
           * 模擬 MySQL AES_DECRYPT() 解密結果 (使用openssl_encrypt)
           * @param binary $crypt_str
           * @param string $key_str 原始的密鑰字串
           * @return string|null
           */
          public function aesDecrypt($crypt_str, $key_str) {
              if (null === $crypt_str || null === $key_str) {
                  return null;
              }
              $cipher = "AES-128-ECB"; //MySQL使用 128bit ECB 演算法
              $options = OPENSSL_RAW_DATA;
              $key = $this->getAESKey($key_str);
              $original_plaintext = openssl_decrypt($crypt_str, $cipher, $key, $options);
              return $original_plaintext;
          }
      
      }
    2. mcrypt,須處理密鑰長度過短、過長,以及加密內容的填充
      /**
       * 模擬 MySQL AES_ENCRYPT()、AES_ENCRYPT()
       */
      class AesMySQL_Old {
      
          /**
           * 原始的密鑰字串
           * @var string 
           */
          private $key_str;
      
          /**
           * 處理後符合規則的密鑰字串
           * @var string 
           */
          private $key;
      
          /**
           * 將原始的密鑰字串,處理成 MySQL AES_ENCRYPT() 使用的密鑰格式
           * @param string $key_str 原始的密鑰字串
           * @return string
           */
          private function getAesKey($key_str) {
              if (isset($this->key_str) && $key_str === $this->key_str) {
                  //此原始的密鑰字串已處理過
              } else {
                  //PHP:只接受剛好 16、24、32 bytes 長度的字串。
                  //MySQL:接受任何長度的字串,
                  //       長度小於16bytes,MySQL在後面用 chr(0) 補齊,
                  //       若超過16bytes,依序每16bytes分成一組,每一組同位置的位元組進行XOR運算,處理成只有16bytes
                  $key_len = 16; //處理成16bytes
                  $key_str_len = strlen($key_str);
                  if ($key_str_len <= $key_len) {
                      $pad = $key_len - $key_str_len;
                      $key = $key_str . str_repeat("\0", $pad); //"\0" 可用 chr(0) 替代
                  } else {
                      $key = substr($key_str, 0, $key_len);
                      for ($i = $key_len; $i < $key_str_len; $i++) {
                          $pos = $i % $key_len;
                          $key[$pos] = $key[$pos] ^ $key_str[$i];
                      }
                  }
                  $this->key_str = $key_str;
                  $this->key = $key;
              }
      
              return $this->key;
          }
      
          /**
           * 模擬 MySQL AES_ENCRYPT() 加密結果 (使用mcrypt_encrypt,PHP7.1以上已不建議使用)
           * @param string $str
           * @param string $key_str 原始的密鑰字串
           * @return binary|null
           */
          public function aesEncrypt($str, $key_str) {
              if (null === $str || null === $key_str) {
                  return null;
              }
              $cipher = MCRYPT_RIJNDAEL_128;
              $key = $this->getAESKey($key_str);
              //使用 mcrypt_encrypt 須自行先將填充做好,避免預設自行填充"\0"
              //(If the size of the data is not n * blocksize, the data will be padded with '\0'.)
              $blocksize = 16; //須為16bytes的倍數
              $text = $this->pkcs5Pad($str, $blocksize); //使用PKCS#5填充
              $mode = MCRYPT_MODE_ECB;
              $encrypted_val = mcrypt_encrypt($cipher, $key, $text, $mode); //ECB沒使用iv
              return $encrypted_val;
          }
      
          /**
           * 模擬 MySQL AES_DECRYPT() 解密結果 (使用mcrypt_encrypt,PHP7.1以上已不建議使用)
           * @param binary $crypt_str
           * @param string $key_str 原始的密鑰字串
           * @return string|null
           */
          public function aesDecrypt($crypt_str, $key_str) {
              if (null === $crypt_str || null === $key_str) {
                  return null;
              }
              $cipher = MCRYPT_RIJNDAEL_128;
              $key = $this->getAESKey($key_str);
              $mode = MCRYPT_MODE_ECB;
              $original_plaintext = mcrypt_decrypt($cipher, $key, $crypt_str, $mode);
              $original_plaintext = $this->pkcs5Unpad($original_plaintext); //去除PKCS#5填充的字元
              return $original_plaintext;
          }
      
          /**
           * 填充不足字節數(PKCS#5)
           * 1.將填充長度取chr()當填充值
           * 2.剛好滿$blocksize倍數,則再填充一組$blocksize大小
           * @param string $text
           * @param int $blocksize
           * @return string
           */
          private function pkcs5Pad($text, $blocksize) {
              $pad = $blocksize - (strlen($text) % $blocksize);
              return $text . str_repeat(chr($pad), $pad);
          }
      
          /**
           * 去除 pkcs5Pad() 的填充值(PKCS#5)
           * @param string $text
           * @return string|false
           */
          private function pkcs5Unpad($text) {
              $pad = ord($text{strlen($text) - 1});
              if ($pad > strlen($text)) {
                  return false;
              }
              if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
                  return false;
              }
              return substr($text, 0, -1 * $pad);
          }
      
      }
  • 使用 PHP 加解密,可減輕 MySQL 負擔、不用在 MySQL Server 執行包含密鑰的 SQL 指令,當然其實加解密方式可以不用做成跟 MySQL 相容。
    但兩者相容的處理方式,若有一天需要直接使用 SQL 指令的 WHERE 條件過濾加密前的資料時,便可派上用場。

參考:

2017年10月10日 星期二

PHP 使用 Twilio 在網頁瀏覽器撥打電話

預計使用瀏覽器的 WebRTC 進行語音對話,所以瀏覽器須支援 WebRTC。
WebRTC 瀏覽器支援狀況:http://caniuse.com/#search=webrtc
注意:Chrome 47以上版本,若不是https加密連線網址,將不允許呼叫 getUserMedia() 使用麥克風、視訊。
  1. PHP Server 端用到的東西,可先參考另一邊文章 「PHP 使用 Twilio 發送簡訊範例」:
    A.下載 Twilio 提供的 PHP SDK
    B.取得 ACCOUNT SIDAUTH TOKEN (https://www.twilio.com/console)
  2. Clinet端瀏覽器則會用到 JavaScript SDK (twilio.min.js)
    我之前使用時,GA(Generally Available)版本為 1.3,所以以下都用1.3版當作範例。
    1.3版網址: https://www.twilio.com/docs/api/client/twilio-js-13
    官方說明為直接外部引用,也可選擇將 twilio.min.js 直接下載回來再使用。
    <script src="//media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js" type="text/javascript">
    官網說明:The twilio.js Library: Twilio in the Browser - Twilio
  3. 登入 Twilio 管理介面,到「Home / Phone Numbers / Tools / TwiML Apps
    新增一個 TwiML App 設定,然後記下 「TwiML App 的  SID
    「FRIENDLY NAME」:自己任意定義的名稱
    「Voice 的 REQUEST URL」:這個很重要,是要給 Twilio 訪問的網址,請填一個之後要開放給 Twilio 訪問的網址。
  4. 下載官方 PHP + JavaScript 範例測試
    GitHub:https://github.com/TwilioDevEd/client-quickstart-php
    下載:https://github.com/TwilioDevEd/client-quickstart-php/archive/master.zip
    解壓縮,並將步驟1 Twilio 提供的 PHP SDK,放到 vendor/ 資料夾下,因範例檔引用路徑是 include('./vendor/autoload.php')。
    最後上傳到可執行PHP的對外Server。
    官方 PHP + JavaScript 範例裡面有幾個檔:index.html、quickstart.js、config.example.php、token.php、randos.php、voice.php,說明如後。
  5. 「index.html」,引用 twilio.min.js (twilio 的 JavaScript SDK工具包)、quickstart.js (JavaScript 使用範例,可隨自己需求修改)
    <!DOCTYPE html>
    <html>
        <head>
            <title>Twilio Client Quickstart</title>
            <link rel="stylesheet" href="site.css">
        </head>
        <body>
            <div id="controls">
                <div id="info">
                    <p class="instructions">Twilio Client</p>
                    <div id="client-name"></div>
                </div>
                <div id="call-controls">
                    <p class="instructions">Make a Call:</p>
                    <input id="phone-number" type="text" placeholder="Enter a phone # or client name" />
                    <button id="button-call">Call</button>
                    <button id="button-hangup">Hangup</button>
                </div>
                <div id="log"></div>
            </div>
    
            <script type="text/javascript" src="//media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js"></script>
            <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
            <script src="quickstart.js"></script>
        </body>
    </html>
  6. 「quickstart.js」,twilio.min.js 使用範例。
    其中這一段會將參數資料傳送給 Twilio,之後 Twilio 訪問第3步驟設定的網址時,也會將這些參數資料一併回傳,所以自訂此處的參數資料,可做一些想達到的效果,例如接收要撥的電話號碼、或是進行自己定義的驗證規則。
    ...
    // get the phone number to connect the call to
    var params = {
        To: document.getElementById('phone-number').value
    };
    
    console.log('Calling ' + params.To + '...');
    Twilio.Device.connect(params);
    ...

    quickstart.js完整內容:
    $(function() {
        log('Requesting Capability Token...');
        $.getJSON('./token.php')
                .done(function(data) {
                    log('Got a token.');
                    console.log('Token: ' + data.token);
    
                    // Setup Twilio.Device
                    Twilio.Device.setup(data.token);
    
                    Twilio.Device.ready(function(device) {
                        log('Twilio.Device Ready!');
                        document.getElementById('call-controls').style.display = 'block';
                    });
    
                    Twilio.Device.error(function(error) {
                        log('Twilio.Device Error: ' + error.message);
                    });
    
                    Twilio.Device.connect(function(conn) {
                        log('Successfully established call!');
                        document.getElementById('button-call').style.display = 'none';
                        document.getElementById('button-hangup').style.display = 'inline';
                    });
    
                    Twilio.Device.disconnect(function(conn) {
                        log('Call ended.');
                        document.getElementById('button-call').style.display = 'inline';
                        document.getElementById('button-hangup').style.display = 'none';
                    });
    
                    Twilio.Device.incoming(function(conn) {
                        log('Incoming connection from ' + conn.parameters.From);
                        var archEnemyPhoneNumber = '+12099517118';
    
                        if (conn.parameters.From === archEnemyPhoneNumber) {
                            conn.reject();
                            log('It\'s your nemesis. Rejected call.');
                        } else {
                            // accept the incoming connection and start two-way audio
                            conn.accept();
                        }
                    });
    
                    setClientNameUI(data.identity);
                })
                .fail(function() {
                    log('Could not get a token from server!');
                });
    
        // Bind button to make call
        document.getElementById('button-call').onclick = function() {
            // get the phone number to connect the call to
            var params = {
                To: document.getElementById('phone-number').value
            };
    
            console.log('Calling ' + params.To + '...');
            Twilio.Device.connect(params);
        };
    
        // Bind button to hangup call
        document.getElementById('button-hangup').onclick = function() {
            log('Hanging up...');
            Twilio.Device.disconnectAll();
        };
    
    });
    
    // Activity log
    function log(message) {
        var logDiv = document.getElementById('log');
        logDiv.innerHTML += '<p>&gt;&nbsp;' + message + '</p>';
        logDiv.scrollTop = logDiv.scrollHeight;
    }
    
    // Set the client name in the UI
    function setClientNameUI(clientName) {
        var div = document.getElementById('client-name');
        div.innerHTML = 'Your client name: <strong>' + clientName +
                '</strong>';
    }
  7. 「config.example.php」,設定檔。
    config.example.php 改名 config.php
    修改 config.php
    $TWILIO_ACCOUNT_SID = 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // 前面取得的 ACCOUNT SID
    $TWILIO_AUTH_TOKEN = 'your_auth_token'; //前面取得的 AUTH TOKEN
    $TWILIO_TWIML_APP_SID = 'APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // 前面取得的 TwiML App SID
    $TWILIO_CALLER_ID = '+1XXXYYYZZZZ'; // 向 Twilio 買的有效的號碼(有語音功能)
    
  8. 「token.php」, JS 一開始會訪問「token.php」,「token.php」會產生給 JS 用的 token
    範例中只有 token是必須的,另一個 identity 可有可無(會出現在 Twilio 的撥號記錄)。
    <?php
    
    include('./vendor/autoload.php');
    include('./config.php');
    include('./randos.php');
    
    use Twilio\Jwt\ClientToken;
    
    // choose a random username for the connecting user
    $identity = randomUsername();
    
    $capability = new ClientToken($TWILIO_ACCOUNT_SID, $TWILIO_AUTH_TOKEN);
    $capability->allowClientOutgoing($TWILIO_TWIML_APP_SID);
    $capability->allowClientIncoming($identity);
    $token = $capability->generateToken();
    
    // return serialized token and the user's randomly generated ID
    header('Content-Type: application/json');
    echo json_encode(array(
        'identity' => $identity,
        'token' => $token,
    ));
    
  9. 「randos.php」,這個檔只是用來產生前面「token.php」的隨機identity,正式應該用不到。
    <?php
    
    // Generate a random username for the connecting client
    function randomUsername() {
        $ADJECTIVES = array(
            'Abrasive', 'Brash', 'Callous', 'Daft', 'Eccentric', 'Fiesty', 'Golden',
            'Holy', 'Ignominious', 'Joltin', 'Killer', 'Luscious', 'Mushy', 'Nasty',
            'OldSchool', 'Pompous', 'Quiet', 'Rowdy', 'Sneaky', 'Tawdry',
            'Unique', 'Vivacious', 'Wicked', 'Xenophobic', 'Yawning', 'Zesty',
        );
    
        $FIRST_NAMES = array(
            'Anna', 'Bobby', 'Cameron', 'Danny', 'Emmett', 'Frida', 'Gracie', 'Hannah',
            'Isaac', 'Jenova', 'Kendra', 'Lando', 'Mufasa', 'Nate', 'Owen', 'Penny',
            'Quincy', 'Roddy', 'Samantha', 'Tammy', 'Ulysses', 'Victoria', 'Wendy',
            'Xander', 'Yolanda', 'Zelda',
        );
    
        $LAST_NAMES = array(
            'Anchorage', 'Berlin', 'Cucamonga', 'Davenport', 'Essex', 'Fresno',
            'Gunsight', 'Hanover', 'Indianapolis', 'Jamestown', 'Kane', 'Liberty',
            'Minneapolis', 'Nevis', 'Oakland', 'Portland', 'Quantico', 'Raleigh',
            'SaintPaul', 'Tulsa', 'Utica', 'Vail', 'Warsaw', 'XiaoJin', 'Yale',
            'Zimmerman',
        );
    
        // Choose random components of username and return it
        $adj = $ADJECTIVES[array_rand($ADJECTIVES)];
        $fn = $FIRST_NAMES[array_rand($FIRST_NAMES)];
        $ln = $LAST_NAMES[array_rand($LAST_NAMES)];
    
        return $adj . $fn . $ln;
    }
    
  10. 「voice.php」,回應 Twilio,Twilio 會根據回應的內容做對應的動作。
    所以回應內容前,也可自己做一些驗證,例如 Twilio 傳送過來的 ACCOUNT SID、TwiML App SID 是否正確,或自己額外傳送的資料是否正常。
    voice.php 的網址須為「Voice 的 REQUEST URL」填的網址。
    <?php
    
    include('./vendor/autoload.php');
    include('./config.php');
    
    use Twilio\Twiml;
    
    $response = new Twiml;
    
    // get the phone number from the page request parameters, if given
    if (isset($_REQUEST['To']) && strlen($_REQUEST['To']) > 0) {
        $number = htmlspecialchars($_REQUEST['To']);
        $dial = $response->dial(array('callerId' => $TWILIO_CALLER_ID));
    
        // wrap the phone number or client name in the appropriate TwiML verb
        // by checking if the number given has only digits and format symbols
        if (preg_match("/^[\d\+\-\(\) ]+$/", $number)) {
            $dial->number($number); //播電話給$number這個號碼
        } else {
            $dial->client($number);
        }
    } else {
        $response->say("Thanks for calling!");//會用語音講"Thanks for calling"
    }
    
    header('Content-Type: text/xml');
    echo $response;
    
    //因要給Twilio 訪問,所以是公開的網址,因此也可以自行加些驗證判斷,不合法就回應404
    //header("HTTP/1.0 404 Not Found");
    
  11. Twilio 發送紀錄:https://www.twilio.com/console/voice/dashboard
    發送失敗debug:https://www.twilio.com/console/dev-tools/debugger


參考:

其他:
Create a Real-Time Video Chat Room with WebRTC & Twilio — SitePoint
https://docs.microsoft.com/zh-tw/azure/partner-twilio-php-make-phone-call
http://twimlets.com/message?Message=test
https://cloud.google.com/appengine/docs/flexible/nodejs/using-sms-and-voice-services-via-twilio
https://www.npmjs.com/package/twilio-js


2017年10月7日 星期六

PHP 使用 Twilio 發送簡訊範例

  1. 下載 Server 端 PHP 的 Twilio SDK(THE TWILIO PHP HELPER LIBRARY)
    官方說明網址:https://www.twilio.com/docs/libraries/php
    可用 Composer 或從 GitHub 下載。
    GitHub:
    https://github.com/twilio/twilio-php
    https://github.com/twilio/twilio-php/archive/master.zip
  2. 登入 Twilio(https://www.twilio.com/) 後,到 Console 頁面(https://www.twilio.com/console) 的 Dashboard,找到 ACCOUNT SIDAUTH TOKEN 兩個值,程式會用到。
  3. PHP 範例
    require __DIR__ . '/Twilio/autoload.php'; // require 下載的 Twilio PHP SDK
    
    $twilio_sid = "....."; //填入前面步驟在 Twilio 的 ACCOUNT SID
    $twilio_token = "....."; //填入前面步驟在 Twilio 的 AUTH TOKEN
    
    $Twilio = new \Twilio\Rest\Client($twilio_sid, $twilio_token);
    $rece_num = "+....."; // 接收者號碼
    $twilio_from = "+....."; // 發送者號碼,須為向 Twilio 買的有效的號碼(有SMS功能)
    $sms_con = "test 測試"; //簡訊內容
    try {
        $message = $Twilio->messages->create(
                $rece_num, array(
            'from' => $twilio_from,
            'body' => $sms_con,
                )
        );
    } catch (\Twilio\Exceptions\TwilioException $ex) {
        //發送失敗
        //"Twilio Exception getCode:" . $ex->getCode();
        //"Twilio Exception getMessage:" . $ex->getMessage();
    } catch (\Exception $ex) {
        // error
    }
    
    if (isset($message->sid)) {
        echo "Twilio message sid:" . $message->sid;
    }
    
  4. 程式使用的電話號碼格式為 E.164 格式


參考:
Sending Messages - Twilio
What is a Message SID? – Twilio Support
SMS Pricing for Text Messaging - Twilio (SMS費用)


2017年10月1日 星期日

硬碟壞軌備份硬碟資料

硬碟出現壞軌時,早期曾用 HDD Regenerator 之類的軟體修復壞軌,但沒多久就又出現壞軌,修幾次後,最終整顆硬碟都沒救。

所以現在只要一發現硬碟 S.M.A.R.T. 出現壞軌資訊時,都會盡量避免繼續使用舊硬碟,立刻買一顆新硬碟,將整個硬碟資料使用 sector by sector 方式複製到新硬碟。

Clone 硬碟的軟體是使用免費版的 EaseUS Todo Backup Free
網址:http://www.todo-backup.comhttps://tw.easeus.com/


新硬碟初始化:
使用 EaseUS Todo Backup Free 10.5 製作的 WinPE 開機光碟操作時,發現一個奇怪的現像,只裝新硬碟時可正常抓到,但若新、舊硬碟同時接上,卻只抓的到舊硬碟,看不到新硬碟。

解決方法是先將新硬碟裝進行初始化,才可同時抓到新、舊硬碟。
例如在 windows「磁碟管理」中初始化磁碟。

在新硬碟上右鍵選擇「初始化磁碟」(一般進去「磁碟管理」就會自動彈出第2張提示初始化的畫面)




建立開機光碟:
  1. 安裝好 EaseUS Todo Backup Free 後,開啟軟體。選擇「工具」->「建立開機碟」
  2. 選擇要用的開機工具與製作方式。
    例如:「建立 WinPE 開機碟」->「建立 ISO」->「執行」,再燒成開機光碟。


複製硬碟資料:
  1. 使用製作的開機光碟開機,可能需一點時間,開機過程畫面如下。


  2. 開機完成後,選擇「Clone」
  3. 選擇來源硬碟(舊硬碟),可選擇複製整個硬碟(disk)或選擇的分割區(partition),這邊是選整顆硬碟。
  4. 選擇目的地硬碟(新硬碟),並勾選「Advanced options」裡的「Sector by sector clone」
  5. 確定設定沒問題後,按「Proceed」開始備份複製。
  6. 複製過程畫面。複製完即可。



參考:
Sector by sector clone - EaseUS Todo Backup online help
How to Clone Drive/HDD/SSD with Bad Sectors - EaseUS

使用 Eraser 抹除硬碟資料,無法刪除未完成的任務

Eraser 可選擇不同演算法抹除硬碟的資料,避免刪除的資料被還原。

之前使用 Eraser(6.2.0.2979)  抹除硬碟資料時,因選的演算法較複雜,覺得要跑很久,想要先停止,再重新設定。

但在「Erase Schedule」中執行「Cancel Task」,卻無法停止,也無法刪除任務。
移除、重開機、重新安裝,都沒用,只要一打開 Eraser,就開始跑那無法刪除的任務。

解決方法:
到「%LOCALAPPDATA%\Eraser 6\」資料夾底下,刪除「Task List.ersy」這個檔案即可。

參考:
Task Won't Complete and Cannot be Cancelled | Eraser Forum