2019年3月26日 星期二

c++ 善用 smart pointer 在物件建構法中傳回不同類型的物件並統一函式介面的作法

每個物件通常有不同的函式介面, 想要將兩種不同物件的方法統一成相同介面, 可以利用物件建構法將它封裝起來, 用這新類型物件去實例化想要的物件, 達成傳回多型物件(polymorphic class)的目的:
#include  <stdio.h>
struct base    { // 基礎類型

  virtual void work( ){ }//擬統一的函式介面

  virtual  ~base(){ } // 避免繼承者,當在解構式中想刪除指標物件時,編譯器會報出錯務訊息

}; // 先建構基礎物件抽象類型, 統一函式介面


struct A: base { // 繼承 base 物件, 將物件用物件 A 封裝起來
    void work( ) override { printf("A\n");    } // 將方法封裝並實現函式介面
};


struct B: base {// 繼承 base 物件, 將另一物件用物件 B 封裝起來
    void work( ) override { printf("B\n");    } // 同樣將方法封裝並實現函式
};


struct C {     // 一個全新多型類型(polymorphic class)
    base *ptr;// 基礎物件的指標, 可以封裝物件 A 或 B
    ~C( )      { delete ptr; }
    C(int a) {  // 根據輸入值實例化不同物件, 奇數傳回 A, 偶數傳回 B, 等同於傳回不同物件
        if (a%2) ptr = (base *) new A();  // 實例化物件 A 指標
        else       ptr = (base *) new B();  // 實例化物件 B 指標
    }
    void work( ){ ptr->work( ); }// 將多型物件成員用指標連結函式介面,方便使用
};
int main(){
   for(int i=0; i < 10; i++)         C(i).work( );
}
如果害怕指標的用法(像是造成 memory leak or double free), 可以用 smart pointer 將它封裝起來, 讓系統自動處理, 就不用擔心記憶體釋放的問題. c++ 所提供的 std::shared_ptr < > 就是 smart pointer 其中之一用來封裝類型指標的標準樣板函式(STL), 但有一點要注意的是, 用 smart pointer 封裝類型時, 該類型的建構及解構方法必須放在 public: 區, 讓 STL 可以正常呼叫, 否則會出現一堆錯誤訊息. 又或者很不喜歡用指標箭頭的方法, 還可以用左值(&alias) 換化成正常物件, 將方法從指標箭頭轉成正常物件的使用方式.
#include  <stdio.h>
#include  <memory> // smart pointer 要使用 memory 函式庫
typedef std::shared_ptr <base> baseptr; // 封裝成 smart pointer, 讓系統自動管理
struct base    {  // 基礎類型

   virtual void work( ){}
   virtual  ~base(){ } // 避免繼承者若在解構式中想刪除指標物件時會報出錯務訊息

}; // 先建構基礎物件的抽象類型, 統一函式介面


struct A: base { // 繼承 base 類型, 可以將物件用類型 A 封裝起來
    void work( ) override { printf("A\n");  } // 將方法封裝並覆蓋虛擬函式
};


struct B: base {// 繼承 base 類型 , 將另一物件用類型 B 封裝起來
    void work( ) override { printf("B\n");    } // 同樣將方法封裝並覆蓋虛擬函式
};


struct C {  // 一個全新多型類型(polymorphic class)
    baseptr ptr;// 宣告 ptr 成為 smart pointer 可以封裝物件 A 或 B 類型的指標    

    // 無需解構式  ~C( ){ }
    C(int a) {  // 根據輸入值實例化不同物件, 奇數傳回 A, 偶數傳回 B, 等同傳回不同物件
        if (a%2) ptr = baseptr(new A( ));  // 實例化物件 A 指標, 同時轉換成 smart pointer
        else        ptr = baseptr(new B( ));  // 實例化物件 B 指標, 同時轉換成 smart pointer
    }

};


int main(){ // 呼叫時, 先用物件直取多型成員, 接著就能用指標 -> 呼叫函式介面
   for(int i=0; i < 10; i++)  C(i).ptr->work( );
}

2019年3月24日 星期日

linux mint 升級 openssl

Openssl:
1. 至官網下載原始碼 https://github.com/openssl/openssl
2. 解壓縮, 並進入目錄
3. 執行 ./config
4. 執行 make
5. 執行 sudo make install
預設程式庫會被安裝至 /usr/local/lib, 導致 openssl 找不到 libssl.so.3 及 libcrypto.so.3 等動態程式庫, 無法執行, 只要將符號連結過去就可以解決問題:
    sudo ln -sf /usr/local/lib/libcrypto.so.3  /usr/lib
    sudo ln -sf /usr/local/lib/libssl.so.3         /usr/lib

Libressl:
1. 這是另一個源出於 Oenssl 的分支, 程式碼是相容的, 號稱刪除一些不必要的程式碼, 讓它更精減, 目標也要讓程式更安全可靠,  編譯的方式大同小異, 參考文章: http://linuxg.net/how-to-install-libressl-2-1-6-on-linux-systems , 可到官網下載源碼:  https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-2.9.0.tar.gz
2. 解壓縮, 並進入目錄
3. 執行 ./configure
4. 執行 make
5. 執行 sudo make install
6. 執行 sudo ldconfig
預設程式庫也是安裝至 /usr/local/lib, 如果導致 openssl 找不到 libssl.so.45 及 libcrypto.so.45 等動態程式庫, 只要將符號連結過去就可以解決問題:
    sudo ln -sf /usr/local/lib/libcrypto.so.45.0.1  /usr/lib/libcrypto.so.45
    sudo ln -sf /usr/local/lib/libssl.so.47.0.1         /usr/lib/libssl.so.45

編譯程式時要加上選項參數  -L/usr/local/lib -lssl -lcrypto, 例如:
g++   https.c -L/usr/local/lib -lssl -lcrypto  -pthread -o https
或是編寫一個簡單的 Makefile
#Makefile to link with ssl, crypto, pthead library
https:
    g++  https.c -L/usr/local/lib -lssl -lcrypto -pthread -o$@
clean:
    rm  -f  https
之後只要下一個命令, 不用再打那麼多字
make

Boringssl:
1. 同樣源自於 Openssl 的另一分支 , 由 google 負責維護的 tls/ssl 程式庫, 首先要安裝 cmake 及 golang 兩個必要開發工具:
     sudo apt-get install cmake golang
2. 到官網用 git 下載源碼, 複製整個 boringssl 目錄
     cd $HOME/Downloads
     git clone https://boringssl.googlesource.com/boringssl
3. 進入目錄, 建個子目錄 build, 進入子目錄 build,  cmake .. ,  make
     cd $HOME/Downloads/boringss
     mkdir build
     cd build
     cmake ..
     make
4. 例如要編譯 c 聯結 Boringssl 程式庫, 可以進到工作目錄, 把編譯好的程式庫複製過來作聯結:
     cd $HOME/work/c/https
     cp $HOME/Downloads/boringssl/build/ssl/libssl.a   .
     cp $HOME/Downloads/boringssl/build/crypto/libcrypto.a   .
     g++   https.c    -L.   -lssl   -lcrypto   -pthread
   

c++ 寫一個簡單的 https server

參考文章: https://wiki.openssl.org/index.php/Simple_TLS_Server
先安裝好 openssl 開發檔及準備自我認証的憑證
sudo apt-get install libssl–dev
openssl req -x509 -nodes -days 30 -newkey rsa:2048 -keyout mykey.pem -out mycert.pem
編輯 https  server 主程式:
// server.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
int main()

    SSL_library_init(); 
    SSL_CTX *ctx = SSL_CTX_new(SSLv23_server_method());
    SSL_CTX_set_ecdh_auto(ctx, 1);
    SSL_CTX_use_certificate_file(ctx, "mycert.pem", SSL_FILETYPE_PEM);
    SSL_CTX_use_PrivateKey_file(ctx,  "mykey.pem", SSL_FILETYPE_PEM);
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    int server = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5000);
    addr.sin_addr.s_addr = INADDR_ANY;
    int  ok = 1;
    setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &ok, sizeof(ok));
    bind(server, (struct sockaddr*)&addr, sizeof(addr));
    listen(server, 10);  
    while(1) {
        int client = accept(server, (struct sockaddr*)&addr, &len);
        printf("Accept client IP= %s : port=%d\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
        SSL * ssl = SSL_new(ctx);
        SSL_set_fd(ssl, client);
        SSL_accept(ssl);
        char buf[1024];
        int bytes = SSL_read(ssl, buf, sizeof(buf));
        if (bytes > 0) { buf[bytes]=0; printf("Client: %s", buf); }
        char msg[] = "<html><head><meta charset='UTF-8'></head><Body>歡迎光臨</Body></html >";
        buf[0]=0;
        sprintf(buf            ,"HTTP/1.0 200 OK\n");
        sprintf(buf+strlen(buf),"Server: Welcome\n");
        sprintf(buf+strlen(buf),"Content-Type: text/html\n");
        sprintf(buf+strlen(buf),"Content-Length: %11ld\n",strlen(msg));
        sprintf(buf+strlen(buf),"Connection: close\n\n");  
        sprintf(buf+strlen(buf),"%s",msg);
        SSL_write(ssl, buf, strlen(buf));
        SSL_free(ssl);
        close(client);  
    };
    close(server);
    SSL_CTX_free(ctx);
}
編譯並執行
g++ server.c -lssl -lcrypto  -o server &&  ./server
用瀏覽器瀏覽  https://127.0.0.1:5000 , 如有警告信息, 選取並信任該憑證

c++ 寫一個簡單的 https client

寫 https 的程式基本上就是將 http 的封包透過 TLS(Transport Layer Security:傳輸層安全協議)/SSL 加密後再傳出去, 收到的封包也是先經過 TLS/SSL 解密後還原.參考文章: https://wiki.openssl.org/index.php/SSL/TLS_Client
設定完 https server(以下使用 local host: 127.0.0.1 TCP port: 5000). 安裝 SSL 開發檔:
sudo apt-get install libssl–dev
編輯一個簡單的 https client 文字檔:
// client.c
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <openssl/ssl.h>
struct sessionTLS {
    int sid;
    SSL_CTX *ctx;
    SSL *ssl;
};
void freeTLS(sessionTLS &tls) {
    if (tls.ssl != NULL) SSL_free(tls.ssl);   
    if (tls.sid > 0)     close(tls.sid);
    if (tls.ctx != NULL) SSL_CTX_free(tls.ctx); 

}
void bindSSL(sessionTLS &tls) {
    tls.ssl = SSL_new(tls.ctx);
    if(tls.ssl != NULL ) {
        SSL_set_fd(tls.ssl, tls.sid);
        SSL_connect(tls.ssl);
        printf("\nsession TLS encryption: %s\n", SSL_get_cipher(tls.ssl));
        X509 * CA = SSL_get_peer_certificate(tls.ssl);

        if ( CA != NULL )  {
            char * subject= X509_NAME_oneline(X509_get_subject_name(CA), 0, 0);
            char * issuer = X509_NAME_oneline(X509_get_issuer_name(CA) , 0, 0);
            printf("CA: %s , %s\n", subject, issuer);
            free(subject);
            free(issuer);
            X509_free(CA);
        }
    }
}
sessionTLS connectIP(const char *ipaddress, int tcpPort){
    SSL_library_init();
    sessionTLS tls;// create a new TLS object
    tls.ctx = SSL_CTX_new(SSLv23_method());
    tls.sid = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in host;
    host.sin_family = AF_INET;
    host.sin_port     = htons(tcpPort);
    inet_pton(AF_INET, ipaddress, &host.sin_addr) ;
    connect(tls.sid, (sockaddr*)&host, sizeof(host));
    bindSSL(tls);// bind tls with SSL
    return  tls;
}
int writeTLS(sessionTLS &tls, char *msg, int len) { return SSL_write(tls.ssl, msg, len); }
int  readTLS(sessionTLS &tls, char *buf, int len) { return SSL_read(tls.ssl, buf, len); }
int main() { 
    char msg[]="Hello\n";
    char buf[1024];
    sessionTLS tls = connectIP("127.0.0.1", 5000);
    writeTLS(tls, msg, strlen(msg));
    int bytes = readTLS(tls, buf, sizeof(buf));

    char *ptr = buf;
    while (bytes-- >0) printf("%c", *ptr ++);
    printf("\n");
    freeTLS(tls);
}

用 g++ 編譯並執行:
g++  client.c -lssl -lcrypto && ./a.out

2019年3月23日 星期六

用 Javascript 寫一個任意位元的亂數產生器


function random(maxnum) {
     // 將產生後的亂數用時間函數運算過, 傳回整數並把它限制在 {0, ..., 最大值-1} 之間:
     let srand = Math.floor(Math.random( )*maxnum) ^ new Date().getMilliseconds();
     return  srand % maxnum ;
}
function srandBits(bits) {
       // 產生任意位元數的亂數
        let r = bits % 8; // 以 8 位元為單位(次), 產生一個亂數
        let num = [ ];     // 初始化儲存陣列
        if (r > 0) {
               num.push(random(1 << r)); // 先產生餘數位元的亂數
               bits -= r;
        }
        while (bits > 0) { // 還剩下的位元數, 一次產生一個位元組(8 bits = 1 byte)的亂數
            num.push(random(256)); // 產生 8 位元的亂數, 0 <= 亂數 < 256
            bits -= 8; // 減掉每次產生的 8 位元
        }
        return num; // 傳回陣列
}
console.log("random:" + srandBits(32));

2019年3月19日 星期二

ECDSA 數位簽章

參考文章: https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
橢圓曲線數位簽章演算法:
假設  ECC(x,y) 是橢圓曲線方程式, 座標產生器 G 將大整數映設到 ECC 曲線上,  使用質數 p 取餘數(%),   1< n < p-1,  假設 order number = n, nG = O
1.  模除數改用橢圓曲線的 order  number  計算
2. 把私章 d, 1 < d < n -1 映射到橢圓曲線的座標 Q = dG = d*G(1) = G(d) ,  Q 就是簽章者的公鑰
3. 用 sha256 擷取文件特徵值 z, z < p,並選一亂數 k, 映設到橢圓曲線 (x, y) = kG = G(k) , 1 < k < n -1
4. 只取出 G(k)的 x 座標,  r = x, 計算出 k 的模倒數 k-1  = 1/k,   (k * k-1) % n = 1
5. 計算 s = (z + rd) / k  %  n =  (z + rd)*k-1  % n,    1 < s < n - 1
6. (z, r,  s , Q)就是帶數位簽章的文件
驗證方式:
1. 數改用橢圓曲線的 order  number  計算,
2. 計算 s 的模倒數 s-1 = 1/s,  (s * s-1) % n  = 1
3. 計算  u = z*s-1  mod n
4. 計算  v  = r*s-1  mod n
5. 用 ECC 加法運算, 計算出座標 K = uG + vQ =G(z*s-1) + G(rd*s-1) = G( (z+rd)*s-1)=G(k)
6. 取出 K 的 x 座標, 應該要等於 r

2019年3月10日 星期日

WebSocket 一些重要觀念

WebSocket  定義在 RFC6455 文件當中, 有兩個重要的章節:
1. 交握協定: https://tools.ietf.org/html/rfc6455#section-1.3
WebSocket 使用的 tcp port 跟 http 相同, 當使用者用 http GET 方法時, 其中攜帶的標頭(header)就包含升級(Upgrade)至 websocket 的動作, 標籤(Sec-WebSocket-Key)後面附帶一串 base64 明文,每次開啟交握協定時都會不同,以一組 base64 明碼為例:
         dGhlIHNhbXBsZSBub25jZQ==
如果將它解開,其實裏面是一組 16 位元組的密碼.假設客戶端所發送封包內容是:
        GET /xxx HTTP/1.1
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
 http 伺服器端收到封包偵測到 Sec-WebSocket-Key, 就將明碼串接一個共通識別碼(GUID)形成長字串:
        dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
用雜湊函數 sha1 計算並轉成 base64 明文簡化成:
        s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
伺服器端產生一個封包把它放在 Sec-WebSocket-Accept: 標籤的後面, 回應用戶端切換通信協定,完成交握程序
        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
2. 封包格式: https://tools.ietf.org/html/rfc6455#section-5.1
一旦交握完成, 雙方封包內就不再用 http 的標頭(header), 而是轉成 WebSocket 格式的標頭, WebSocket 客戶端送的資料必須使用 xor 加密, 所附帶 4 位元組(byte)遮蔽碼負責把資料每 4 個位元組循環作加密運算(xor)轉成亂碼,伺服器端則利用這 4 個遮蔽碼同樣每 4 個位元組循環作解密運算(xor)轉成明文.
標頭第 1 個位元組分上半部與下半部:
        上半部最高位元(MSB) 標示該封包是否是最後一筆封包, 後 3  個位元保留未使用
        下半部 4 個位元代表該封包的格式(opcode),分別如下:
            0000  表示是上一封包的延續
            0001 表示文字格式(ascII code)
            0010 表示16進制(binary code)
            1000 表示關閉 Websocket 通道
            1001 表示 ping 封包
            1010 表示 pong 封包
            其他保留未使用
        用 c++ 語言描述:
        bool  fin   =   p[0] & 0x80;
        auto opcode = p[0] & 0xf ;
        if( fin ) printf("final frame\n");
        switch(opcode){
          case 0 : printf("continue frame\n") ;break;
          case 1 : printf("text frame\n")      ;break;
          case 2 : printf("binary frame\n")  ;break;
          case 8 : printf("close frame\n")    ;throw "WebSocket: Connection close!";
          case 9 : printf("ping frame\n")     ;break;
          case 10: printf("pong frame\n")  ;break;
          default: printf("reserved frame\n") ;break;
        } 
標頭第 2 個位元組指示隨後的資料, 最高位元(MSB)代表是否資料經過 xor 運算, 剩下7個位元是資料長度數值, 當數值小於 126, 就當成是資料長度(payload length), 如果等於 126, 那要用隨後的另兩個位元組當資料長度, 但如果大於 126(也就是等於 127)時, 就用隨後 8 個位元組當資料長度.客戶端送來的資料必須用 xor 運算過, 因此資料長度後面,必定先緊隨 4 個位元組的遮蔽碼(mask key), 其後才是資料(payload). 而伺服器傳給客戶端並不需要加密, 也就無需遮蔽碼, 因此長度後面緊跟的就是資料(payload)了, 簡單用 c++ 語言來解碼:             
        if (p[1] & 0x80) { // maskbit should be true
           p[1] &= 0x7f; // data in buffer will be de-mask
           auto wslen = p[1];// length value copy
           p += 2; // don't need the first 2 bytes in header
           long long payloadlen;// 8 bytes integer to get payload length
           if (wslen < 126 )  payloadlen = wslen;
           else { // 126: following 2bytes, 127: following 8 bytes
               payloadlen = *p ++;
               payloadlen = (payloadlen << 8) + *p ++;     
               if (wslen>126)  for (auto i=0; i < 6; i++) payloadlen = (payloadlen << 8) + *p ++;
           }
           unsigned char maskkey[4]; // to store maskkey
           for (auto i=0; i < 4; i++ ) maskkey = p[i]; // maskkey copy
           for (auto i=0; i < payloadlen; i++) p[i] = p[i+4] ^ maskkey[i%4]; // decode & move
           size -= 4; // size of buffer was reduced by 4 bytes of maskkey
           auto &payload = p; // payload now point to data
        }
3. 客戶端可以從 http 伺服器端接收一個 index.html 檔案, 編寫的內容像是:
<html><head><title>WebSocket 發送與接收示範</title><meta charset="UTF-8"></head>
    <script>
     const ws = new WebSocket('ws://127.0.0.1:5000');// port 必須與 http 相同
      ws.addEventListener('open', () => {
           msg = "Hello, thhis is a test message.";
           console.log("Send:  " + msg);
           ws.send(msg);
       });
       ws.addEventListener('message', event => {
           console.log("Get::  " + event.data);
           ws.close();
       });
    </script>
    <body>打開瀏覽器的 Console 視窗觀看結果</body>
</html>
內嵌的 Javascript 是一個用來開啟 WebSocket 通道的程式碼, 假設 http 伺服器使用 tcp port = 5000, 將上述的 index.html 放在伺服器的 www 根目錄底下, 用瀏覽器開啟 URL 的頁面 http://127.0.0.1:5000/index.html