淺談TOTP(Time-based One-time Password)

TOTP 是利用 Application and services 上時間是一致,拿二邊共同的 key ,並且作一樣的 Hash 後,結果會產生一樣的 token,即可拿來作驗證,常被使用來作二次驗證(two-step verification)。

因為這部份的演算法是 RFC6238,所以可使用任何的 apps 來協助驗證,例如:Google Authenticator, Duo 之類的。

怎麼實作的

PHP 有人作好了: otphp

跟據 wikipedia 裡的 Implementation 章節中,有一段描述如何產生 token:

  1. Calculate C as the number of times TI has elapsed after T0.
  2. Compute the HMAC hash H with C as the message and K as the key (the HMAC algorithm is defined in the previous section, but also most cryptographical libraries support it). K should be passed as it is, C should be passed as a raw 64-bit unsigned integer.
  3. Take the least 4 significant bits of H and use it as an offset, O.
  4. Take 4 bytes from H starting at O bytes MSB, discard the most significant bit and store the rest as an (unsigned) 32-bit integer, I.
  5. The token is the lowest N digits of I in base 10. If the result has fewer digits than N, pad it with zeroes from the left.

如果覺得太復雜,我挖了一下 otphp,應該在 otphp/src/OTP.php 裡面的 protected function generateOTP($input) 中有實作。

使用

安裝 composer require spomky-labs/otphp

sample code:

<?php
require __DIR__ . '/vendor/autoload.php';
use OTPHP\TOTP;

$totp = new TOTP(
    "whatup.tw@gmail.com" // The label (string)
);
$google_chart = $totp->getQrCodeUri();
$otpCode = $totp->now();
echo "<img src='{$google_chart}'><br/>";
echo "Current OTP: " . $totp->now() . "<br/>\n";
echo "Current Secret: " . $totp->getSecret() . "<br/>\n";
echo "Verify OTP: " . $totp->verify($otpCode) . "<br/>\n";
?>

到 browser 執行後會產生下面畫面:
totp example result

接下來你就拿出你的 Google Authenticator 來掃這個條碼,就可以新增一組新驗證碼。

Google Authenticator 預設是 30s ,所以 30s 後,這個 OTP 產生出來的 token 應該會無法使用。如果你的 application 可以看到 secret 的話,應該會看到 app & service 的 secret 應該要一樣。

剩下的可以參考 document 說的很清楚。

限制

  • 其實無法防止 phishing 網頁用假的頁面來騙你資料,在你輸入 token 後,壞人在 30s 之內也可以同時登入。
  • secret key 如果被人拿走,別人也同時擁有通過二次驗證的能力。
  • app & services 之間的時間一定不能差太多,所以手機不能調整時間(有人喜歡看時鐘快五分鐘)。

Let’s Encrypt 免費讓你的網站升級成 SSL ready

Our Commitment to Protecting Your Information by Marissa Mayer, Yahoo CEO

2013 年底時,美國方面爆出政府直接拉線路進 Yahoo 機房偷取使用者資料,所以在2014 年初時 Yahoo 宣布全面使用 SSL 連線來保護連線資料。但買一個 SSL 憑證實在是太貴了,如果一堆 domain 的話,更是負擔不起。所以在自已用的小站通常都用 self-sigh 來解決這個問題,只是使用時會一直被 Browser 靠腰說你的 SSL 是不合法的。

時至今日, Internet Security Research Group (ISRG) 組織為了網路安全,提供了免費的 SSL 申請服務 - Letsencrypt ,主要有下列幾項特色:

  • Free: Anyone who owns a domain name can use Let’s Encrypt to obtain a trusted certificate at zero cost.
  • Automatic: Software running on a web server can interact with Let’s Encrypt to painlessly obtain a certificate, securely configure it for use, and automatically take care of renewal.
  • Secure: Let’s Encrypt will serve as a platform for advancing TLS security best practices, both on the CA side and by helping site operators properly secure their servers.
  • Transparent: All certificates issued or revoked will be publicly recorded and available for anyone to inspect.
  • Open: The automatic issuance and renewal protocol will be published as an open standard that others can adopt.
  • Cooperative: Much like the underlying Internet protocols themselves, Let’s Encrypt is a joint effort to benefit the community, beyond the control of any one organization.

這項服務在 2015/12/3 已經 Open Beta 了,任何人可以直接使用,不需申請。

Install

使用方法也很簡單到 github 下載 letsencrypt client 後,直接執行 ./letsencrypt-auto --help all 就可以看到所有的說明和使用方法。

Renew

預設的 SSL 90 天後就會過期,所以我就排個 cron 每天去跑一次。
這個 Script 會刮出所有 apache2 底下有開 443 port 的 servername 去 renew SSL。其中 --renew-by-default--agree-tos 開啟後 CLI 介面不會問東問西的選項。

#!/bin/sh
/usr/sbin/apachectl stop

/home/whatup/letsencrypt/letsencrypt-auto certonly -a  standalone --renew-by-default --agree-tos `grep -ih servername /etc/apache2/sites-enabled/*|grep 44
3 | sed 's/:443//g' | sed 's/ServerName/-d/g' `  --email whatup.tw@gmail.com

/usr/sbin/apachectl start

Rate Limit

流量限制如下,自已要多加注意,要不然會被檔掉。

  • Registrations per IP is 10 registrations per 3 hour window.
  • Certificates per name is 10 certificates per 59 days.
  • Pending registrations per account is 300 per 1 week.

Result

看到綠色合法的 SSL Icon 就是開心!
Screen Shot 2015-12-03 at 9.47.04 AM

Screen Shot 2015-12-07 at 10.24.43 AM
Screen Shot 2015-12-07 at 10.25.09 AM
Screen Shot 2015-12-07 at 10.25.30 AM
Screen Shot 2015-12-07 at 10.24.56 AM

[php] grapheme_strlen vs mb_strlen vs strlen

前言

小時候在寫 PHP 時要看字串長度,常常不懂事的用 strlen 來作判斷,但是在遇到中日韓這種會把 2 bytes 當一個字的長度時,這時候字串的長度又不準了。這時學會了使用 mb_strlen 這個 function,感覺長度這樣用,就準了。但有趣的事是,竟然近期又出現了 grapheme_strlen 。崩潰,不過就是算個長度而已,為什麼有這麼多的方法。

讓我們來寫個小程式來實驗一下

<?php
$str = "這是中文 english 混合測試";
var_dump($str);
var_dump(
    mb_strlen($str, 'UTF-8'),
    grapheme_strlen($str),
    strlen($str)
);
?>

結果如下:

$ php /tmp/test.php
string(33) "這是中文 english 混合測試"
int(17)
int(17)
int(33)

看起來很合理啊,用 strlen 結果是 33 個字元,因為他是算 bytes。mb_strlen 17 個字,即使是中英文混合,看起來算字數也是沒問題的!mb_strlen 和 grapheme_strlen 目前看起來結果是一樣的。

接下來再一個小實驗

<?php
$str = json_decode('"e\u0301 = \u00E9"');
var_dump($str);
var_dump(
    mb_strlen($str, 'UTF-8'),
    grapheme_strlen($str),
    strlen($str)
);
?>

結果如下:

$ php /tmp/test.php
string(8) "é = é"
int(6)
int(5)
int(8)

WTF,竟然有三個不同的數字,畫面上來看,明明就只有五個字,為什麼 mb_strlen 會算出來六個字呢!

原來是 Grapheme Cluster 在作怪

Grapheme Cluster 就類似中文一樣,會把二個字當作一個字。所以畫面上看起來是一個字,但實際上在 unicode 是可以用二個字來表示,所以可以看見範例二中顯示的字是一樣,但 unicode 是不同的。而這樣就會造成 mutlibyte 的 function 計算長度有問題。

結論就是....

如果有考慮中日韓以外的語系的話,可以使用 grapheme_strlen 。另外必須要注意,如果傳進來的參數不是 utf8 的字的話,他的回傳值是 null ,而這部份與 mb_strlen 的結果不一致。

參考文章

[php] mock 有 reference parameter 的 method

前言

雖然盡量不要在 php 裡面寫 call by reference 的參數,但是有一些舊有的code 還是有人這樣寫。如果這時又想要mock 他就麻煩了,因為值並不會隨著自已想要的內容作改變。

實作

原先的 class 設計如下

<?php
class NeedToMockClass {
    function testReference(&$str) {
        // 也許會有很多很複雜的計算,不過這邊簡化成一行就好    
        $str = "test String";
        return;
    }
}
class testClass{
    function test($str) {
        $t = new NeedToMockClass();
        $t->testReference($str);
        return __METHOD__ .'->' . $str;

    }
}
// $t = new testClass();
// $str = $t->test('not a test string');
// var_dump($str);
// string(28) "testClass::test->test String"

How To Test?

如果這時我要測 testClass 我該如何測?因為我單純的只想測 testClass 裡的邏輯對不對,我不想管 NeedToMockClass 的邏輯,所以這時我該把 class mock 掉,並且讓他可以自定 $str 的 output。

首先,會先改寫 testClass 讓他可以從外面傳 NeedToMockClass 的 object 進去。

<?php
class testClass{
    private $_mockClass = null;
    
    // 由 construct 傳進來,讓他可以取代掉預設的 mockClass
    function __construct($mockClass == null) {
        if ($mockClass = null) {
            $mockClass = new NeedToMockClass();
        }
        $this->_mockClass = $mockClass;    
    }
    function test($str) {
        // 接著用mock 後的class 來執行    
        $this->_mockClass->testReference($str);
        return  __METHOD__ .'->' . $str;

    }
}

最後測試程式會用下面這個方法寫,主要是用 returnCallback 的方式來 mock:

<?php
require_once './sample.php';
class testTest extends PHPUnit_Framework_TestCase
{
    function mockTestReference(&$str)
    {
        $str = '3345678';
    }
    function testTestClass()
    {
        $mockClass = $this->getMock('NeedToMockClass', array(), array(), "" ,false);
        // 這裡是用 returnCallback 的方式呼叫 testTest::mockTestReference 這樣就可以改 $str 的內容了
        $mockClass->expects($this->any())
                  ->method('testReference')
                  ->will($this->returnCallback(array('testTest', 'mockTestReference')));
        $t = new testClass($mockClass);
        $str = 'one test';
        $ret = $t->test($str);
        // 最後會如預期的產生 testClass::test->3345678 的字樣
        $this->assertEquals('testClass::test->3345678', $ret);
    }
}

順利達成目標,其結果如下:

$ phpunit testTest.php

PHPUnit 3.7.35 by Sebastian Bergmann.

.

Time: 22 ms, Memory: 3.50Mb

OK (1 test, 2 assertions)

參考文件