commit 7fc247b152d95d221682c662b24a0442c146c752 Author: Inndy Date: Wed May 20 00:11:26 2020 +0800 First public release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..711812a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +/venv +/swigwin-4.0.1 +/swigwin-4.0.1.zip diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8763f34 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +This file is part of twnhi-smartcard-agent. + +twnhi-smartcard-agent is free software: you can redistribute it and/or +modify it under the terms of the GNU General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +twnhi-smartcard-agent is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with twnhi-smartcard-agent. +If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb464b5 --- /dev/null +++ b/README.md @@ -0,0 +1,229 @@ +# TWNHI Smartcard Agent + +## 這是什麼? / What is this? + +這是一個可以取代 +[健保卡讀卡機元件](https://cloudicweb.nhi.gov.tw/cloudic/system/SMC/mEventesting.htm) +的程式,使用 Python 重新撰寫,避開了原始實作的軟體缺陷,提供更好的品質與文件。 + +## TODO + +- [x] 增加 socks5 proxy 伺服器,攔截 `iccert.nhi.gov.tw` 的連線 / + Add socks5 proxy to hijack connection to `iccert.nhi.gov.tw` +- [ ] 完善文件 / Finish documents +- [x] 驗證 client 來自 `*.gov.tw` / Limit the connection was came from `*.gov.tw` +- [ ] 預編譯 `pyscard` 套件 / Prebuild `pyscard` package +- [ ] 製作 prebuilt package 並加入 GitHub releases / + Prebuild package and add to GitHub releases +- [ ] 蒐集使用回饋 / Collect usage feedbacks + +## 相依套件 / Dependencies + +- `python>=3.6` (Only tested on Python 3.8.0) +- `openssl>=1.1` +- `virtualenv` +- `requirements.txt` 檔案內列出的 Python 套件 +- Windows 使用者需要 Visual Studio 或者 + [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) + +## 我很懶,給我懶人包 / TL;DR +``` +# Windows (PowerShell) +> Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force +> python.exe install-packages.py +> .\venv\Scripts\activate.ps1 +> python.exe server.py + +# Linux (Ubuntu) +$ sudo apt-get install libpcsclite-dev + +# Linux and macOS +$ brew install swig # homebrew/linuxbrew +$ python3 install-packages.py +$ source ./venv/bin/activate +$ python3 server.py +``` + +## 安裝方式 / Setup + +### 範例指令格式 / Note for the commands listed below +``` +> python.exe --version # this command is for Windows +$ python3 --version # this command is for Linux and macOS +``` + +### 確認 Python 版本大於等於 3.6 / Check python version +``` +> python.exe --version +$ python3 --version +Python 3.8.0 +``` + +### 確認有沒有安裝好 virtualenv / Check virtualenv +``` +> python.exe -m virtualenv +$ python3 -m virtualenv +python3: No module named virtualenv +``` + +### 安裝 virtualenv / Install virtualenv +``` +> python.exe -m pip install virutalenv +$ python3 -m pip install virutalenv +(很長的安裝畫面... / Installation progress...) +``` + +### 確認有沒有安裝好 virtualenv / Check virtualenv again +``` +> python.exe -m virutalenv +$ python3 -m virtualenv +usage: virtualenv [--version] [--with-traceback] [-v | -q] [--app-data APP_DATA] [--clear-app-data] + [--discovery {builtin}] [-p py] [--creator {builtin,cpython3-win,venv}] + [--seeder {app-data,pip}] [--no-seed] [--activators comma_sep_list] [--clear] + [--system-site-packages] [--symlinks | --copies] [--no-download | --download] + [--extra-search-dir d [d ...]] [--pip version] [--setuptools version] + [--wheel version] [--no-pip] [--no-setuptools] [--no-wheel] [--symlink-app-data] + [--prompt prompt] [-h] + dest +virtualenv: error: the following arguments are required: dest +``` + +### 建立一個 virtualenv / Create a virtualenv +``` +> python.exe -m virtualenv venv +$ python3 -m virtualenv venv +created virtual environment CPython3.6.8.final.0-64 in 3169ms + creator CPython3Posix(dest=/code/venv, clear=False, global=False) + seeder FromAppData(download=False, pip=latest, setuptools=latest, wheel=latest, via=copy, app_data_dir=/home/user/.local/share/virtualenv/seed-app-data/v1.0.1) + activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator +``` + +### 啟動 virtualenv / Activate virtualenv we just created +``` +> .\venv\Scripts\activate.ps1 # Windows with Powershell +$ source ./venv/bin/activate # Linux and macOS, I assume you are using a POSIX shell +``` + +#### PowerShell 錯誤 / PowerShell Error + +PowerShell 預設不允許執行不信任的 script,如果你發生以下錯誤,請執行 +`Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force` +,這將會允許現在的 PowerShell process 執行外部 script + +Default config of PowerShell does not allow external script execution. +Execute `Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force` +to temporarily allow execution of external script. + +``` +PS C:\code> .\venv\Scripts\activate.ps1 +.\venv\Scripts\activate.ps1 : 因為這個系統上已停用指令碼執行,所以無法載入 C:\code\venv\Scripts\activate.ps1 檔案。如需詳細資訊,請參閱 about_Execution_Policies,網址為 https:/go.microsoft.com/fwl +ink/?LinkID=135170。 +位於 線路:1 字元:1 ++ .\venv\Scripts\activate.ps1 ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : SecurityError: (:) [], PSSecurityException + + FullyQualifiedErrorId : UnauthorizedAccess +PS C:\code> Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force +PS C:\code> .\venv\Scripts\activate.ps1 +(venv) PS C:\code> +``` + +### 安裝 Swig / Install Swig +[pyscard](https://github.com/LudovicRousseau/pyscard/blob/master/INSTALL.md#installing-on-gnulinux-or-macos-from-the-source-distribution) +需要使用到 [Swig](http://www.swig.org/) 來產生 Python native extension + +macOS 使用者推薦使用 [Homebrew](https://brew.sh/) 來安裝套件,Linux 使用者也可以 Homebrew +([Linuxbrew 目前已經與 Homebrew 合併](https://github.com/Linuxbrew/brew/issues/612)), +使用發行版自帶的套件管理工具 (apt, rpm, pacman... etc.) 來安裝 swig。 +``` +$ brew install swig # Use homebrew/linuxbrew to install swig +``` + +### 安裝需要的套件 / Install python packages +``` +$ sudo apt-get install libpcsclite-dev # Linux need libpcsclite-dev, apt-get is for Ubuntu +$ pip install -r requirements.txt +``` + + +## 使用方式 / Usage + +1. 啟動 virtualenv / Activate the virtualenv +2. 執行 server.py / Run server.py +3. 設定瀏覽器使用 socks5 proxy 127.0.0.1:17777 / + Config your browser to use 127.0.0.1:17777 as socks5 proxy + +### 設定瀏覽器使用 socks5 proxy / Config your browser to use socks5 proxy + +#### Chrome + +- [Proxy SwitchyOmega](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif) +- [FoxyProxy Standard](https://chrome.google.com/webstore/detail/foxyproxy-standard/gcknhkkoolaabfmlnjonogaaifnjlfnp) + +See [How to setup socks5 proxy in Chrome with FoxyProxy](docs/setup-socks5-proxy-chrome-foxyproxy.md) + +#### Firefox + +> TBD + +### 設定系統信任 root certificate / Config your system to trust our root certificate + +#### Windows +``` +> cd certs +> .\trust_ca.cmd +``` + +#### macOS +``` +$ cd certs +$ ./trust_ca_macos.sh +``` + +### Linux +``` +# For Ubuntu and Firefox +$ sudo apt-get install libnss3-tools +$ cd certs +$ ./trust_ca_ubuntu_firefox.sh +``` + +關閉瀏覽器,再重新開啟設定才會生效 + +### 啟動伺服器並進行測試 +``` +> python server.py +``` + +設定好 socks5 porxy,並且用瀏覽器開啟 +[https://iccert.nhi.gov.tw:7777/](https://iccert.nhi.gov.tw:7777/) + +正確設定的狀況下應該不會看到任何錯誤,並且看到 `It works!` 就表示 agent 啟動成功 + +## 資訊安全考量 / Security Issue + +### 自簽憑證 / Self-signed Certificate + +由於健保卡讀卡機元件使用 wss (WebSocket Secure) 通訊協定,因此必須要有 SSL/TLS 憑證, +目前健保署並未提供 `iccert.nhi.gov.tw` 的有效憑證,因此我們使用自簽憑證來處理這個問題。 + +為了使用方便,安裝步驟中會引導使用者在系統上安裝並信任自簽根憑證,為了使用者的方便, +已經有一組預先產生好的憑證可以使用,為了確保該憑證不會被濫用,我們已將根憑證的私鑰銷毀。 + +若您希望有更高的安全性,可以參考 certs 目錄底下的 Makefile,裡面有使用 openssl +重新產生一組私鑰與憑證的方法,自行產生自己的根憑證與網站憑證, +再銷毀根憑證的私鑰來保證自簽根憑證不會遭到竊取與盜用。 + +以下是重新產生憑證的步驟: + +``` +> cd certs +> make clean +> make all +# 現在可以參考上面的步驟,讓系統信任剛剛產生的 CA +> ./trust_ca_macos.sh # 以 macOS 為例 +``` + +## 授權 / License + +[GPL v3](LICENSE) diff --git a/certs/.gitignore b/certs/.gitignore new file mode 100644 index 0000000..dc0bca8 --- /dev/null +++ b/certs/.gitignore @@ -0,0 +1,7 @@ +*.crt +*.key +ca.srl +!ca.crt +!chain.crt +!host.crt +!host.key diff --git a/certs/Makefile b/certs/Makefile new file mode 100644 index 0000000..740ccdf --- /dev/null +++ b/certs/Makefile @@ -0,0 +1,36 @@ +DAYS ?= 730 + +all: host.crt check chain.crt + +clean: + rm ca.key ca.crt host.key host.crt host.csr chain.crt + +finalize: host.crt + rm ca.key + rm chain.crt + $(MAKE) chain.crt + : Now you can trust ca.crt in your system, and nobody can abuse this root CA + +ca.key: + openssl genrsa -out ca.key 4096 + +ca.crt: ca.key + openssl req -x509 -new -nodes -key ca.key -sha256 -days $(DAYS) -out ca.crt -subj "/C=TW/ST=Taiwan/O=Inndy's NHI Smartcard Client" + +host.key: + openssl genrsa -out host.key 4096 + +host.csr: host.key + openssl req -new -key host.key -config san.cnf -sha256 -out host.csr + +host.crt: host.csr ca.crt ca.key + openssl x509 -req -in host.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out host.crt -days $(DAYS) -sha256 -extensions req_ext -extfile san.cnf + +chain.crt: + cat host.crt ca.crt > chain.crt + +check: + : ==================== ca.crt ==================== + openssl x509 -noout -text -in ca.crt + : ==================== host.crt ==================== + openssl x509 -noout -text -in host.crt diff --git a/certs/ca.crt b/certs/ca.crt new file mode 100644 index 0000000..d6b0bbe --- /dev/null +++ b/certs/ca.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFBjCCAu4CCQCRhyBUzRq1gjANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJU +VzEPMA0GA1UECAwGVGFpd2FuMSUwIwYDVQQKDBxJbm5keSdzIE5ISSBTbWFydGNh +cmQgQ2xpZW50MB4XDTIwMDUwNjE0NTMzM1oXDTIyMDUwNjE0NTMzM1owRTELMAkG +A1UEBhMCVFcxDzANBgNVBAgMBlRhaXdhbjElMCMGA1UECgwcSW5uZHkncyBOSEkg +U21hcnRjYXJkIENsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AK5KShoUDkJ1U8kvzISbDTk9z1QMYGo/jNk3oqL3YS4nhFQt9VSIN8k0Z+ehCKlV +tDQK0sWVUPG8f/lVtKV+MSGMOuQTp0rtUYpTRrbmv/FblF5atI0c3jo0beP47fH7 +1qTYFZep7tR4PFG9U9kDsQCR0nam35ydqq9j/ehBp6/QGAg1gMrhuxj29VefpE9+ +GQXEM41MUIW53ZOlRB8MJkFCfxwfxUbMfuA/JdAkmiRXpM6IpffVfSdhISQPtv6O +ZPs2yHxYp7HOuV7XOxV/34o/A/wiEjoz56eoH+oslgjzB/enk9oBSwkAfYL1FFDN +hWNspkRLEhairRbQuve46l3i7g0jVxUcVIV6PcyC+C9Ci2L9dGX/HBlgmfo8FlX7 +ai2NDujjWGxGp3iFa7wvU/sDZar8ViEUo4FKKgPOY1cRpalBi8uEFXfS9FRh9ckV +rxZDxWsybLwkA+JrfNtKi5XAbJyoHQSG8f7XazuSsh7aLFMNkaHZ1lks3y2nWB+M +z5a89P8Lpj9AXFr/M3LWn9RFg7u+2LCLGFiUmaHG/JfDKxlDTyFh2K+UrgG2c4Ek +9EuehsUVjmJCoMZ1inVDkPixFNuRvlh+TTI0cU+ULVUFtdU5heOuaXXNHV5UDHID +8gucoNJb1aqpNoMtSDNYd5aaxuGwgwMHL/RjwC7lWOj1AgMBAAEwDQYJKoZIhvcN +AQELBQADggIBABibU0zYYGogbwDf+xj/38D9KdpedIW2ZL86ExQyySYgdZm2eOTa +e+qVVIzVOoKQFPtIAXX07M7EDJVIte79eyZnAHWNA0QEgTGBwrltybFyN+PQNpjS ++b+2YsiqB4mtxmkOf4uypT+cUwYBw2Y/yD69lPHtsbYcboud20qzsw28/72sShUY +yfJ9E0BtjsagoTNniUnXx6O3uIN9nzgdC98n9qTRXQqo1YoidsoK+nnY+g0jAYNh +QzyPxinppbM9TGpEWhs1FGaQasTEX/eCbePwPvb5zRKDBplTg1IAATx9njzhhmet +hrYj9elEsWCwV0d7NMp7zbr8pBHAdbvs3DpcPMKpSNA4918Ak89dfRXuKxyvsbRE +e3tr0pLL2NC+9V2C2yj+U/lf+8xUzwBxSbgEjStmwdVPtJj2897iNSCDoKlC3vlY +uaJL0ShEp4FNpdUwa1RB9dSSFeYC/s/FNgk/pcEblE820rP/Y+IMtcmYcTg5c7ta +0g3Scl+JBp0y3fuGhSSZ7iyhqM6RZEKHYTnLbE1mOKx1wAhb0pQ9UC0+EbT/elJs +5EtcLtmgLidA2YKc7u4uJhNvmIIYKE1XQNytsP4PxGjlYJh5C+QzqxwcjfIza+Xc +r/a5yG+yjvxet0lvr04JgHQjvYW6Sqvwn8J+Ioeh8+ZCx/cteEQg9gpl +-----END CERTIFICATE----- diff --git a/certs/chain.crt b/certs/chain.crt new file mode 100644 index 0000000..fe7f533 --- /dev/null +++ b/certs/chain.crt @@ -0,0 +1,60 @@ +-----BEGIN CERTIFICATE----- +MIIFRDCCAyygAwIBAgIJAN6l3sQW0+nHMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAlRXMQ8wDQYDVQQIDAZUYWl3YW4xJTAjBgNVBAoMHElubmR5J3MgTkhJIFNt +YXJ0Y2FyZCBDbGllbnQwHhcNMjAwNTA2MTQ1MzMzWhcNMjIwNTA2MTQ1MzMzWjBE +MQswCQYDVQQGEwJUVzEPMA0GA1UECAwGVGFpd2FuMSQwIgYDVQQKDBtJbm5keXMg +TkhJIFNtYXJ0Y2FyZCBDbGllbnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCufr1KhcY3TcASUwv9RyidgKf9FESn+r97V+QGFpMhewxtAtVnpZIfNr5U +Mvz1sIQ1MmKur2bvcxLrRjfrncJHKmJpBJscZGvViIV4ziIi9bdvpQHVdp4qrLwV +lJOFG5bCirKRuTeC0HRoZ6/1S71NzBwpKsgcoXLmd6PoqUFih6qphoWgN6yo5PQo +/HbHszaVrcTGwdsQBaWzoTihRDeC9lzuRSN4s+3jJvh7H0GRlFsi9suK2KJaQXw9 +BrRGD34gPJMYjaTRTVUyDAI0bW1vsN0GAtKxTfoB3VRCIEbP4DUtOPYI9e0rw2JA +t8oUd4gvaRWTSakOMuBPLssrJaFaQ0CHVrsvuoXJ8gSHPu4pFX0MvPLwXDpvyFWD +Z9B8u7PwIc39Jtq1WAgS/i6+xNQzo7djp+Jzgh8VuMIg1iyzV+jJIhy1WdyoKXl6 +p3cg5DjzF09F3s9Of/3rHpVPnoSwrvKOG/ND2l/TQyEOV9JuStqO27wOZudAd42G +oJX8dn9P/r+MPtOPBFFK1J8jm28aK7KptXN84gV/IH6nEHf9vAcDHy/RaQE9fIYu +pudTWwAhgSdH5i4QcPlpXpQdzSD7PC9uUEdiCHaeze4UWc9N0Qw+oH93bN2MqxsU +Y/9SqfTWVCKQAs4WKF7+C3sB1+gfs0P2/RDuOtHJfSQVQSrlfQIDAQABozgwNjAJ +BgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAcBgNVHREEFTATghFpY2NlcnQubmhpLmdv +di50dzANBgkqhkiG9w0BAQsFAAOCAgEALf5LBL74y9uupYrxYWkzvnWwAnwEVQ1V +g2ygBP7ldrCQIPof69UuWZ2CwN70NwH3AtI6vd2cUBpH2AYyaJe3qYsfsq+/xpdv +FB5xVoIjA3MBuzZWW+PZhtTQ41pzsuKH5MG70SYk/y27/VqQmFTzNBAXLc1130sn +o6uJflYcVE+XxfEj901Zz1BdIcpfu44bqz8ilAaDU6bG4IOBnKQ18GNbfO7Fj+Ei +28UcoGxaiiAcCIektZitaNKjVb4U6uIpNuxhAHxPCZq24z19Iwa4+1eI1GzoxhCW +SFmAZFuF26KVqAOH5Ewo9AlAQ59L99vuhHpyhdt4bXyJCGKYVTJMkEf2oLTaOLoX +1aEpIyCDFY611jJBEEGssajcpZBQWuaK1zTCHUZWp+l1Tme90C5XRPfnCbuei31o +SRK+mDXh1JRWyJxIelta46X3tTN+rEzSnMjUeR1dto8Z54iXu3VctcOfZFrUsKMB +uiwLtN6vYT0jIqlI13C24ybePn6v+1HIc29KNblQVwBpBcU2zcQ6Y2/DAL4NSDBS +hmET24hK/6RpBW+foWxRK6Jl2MSbj+thRBIGKQcAMGs0WOaVk9DNqvOgYGJFixec +HiKJusY06F6ecvC0aaKmbX2kRJf1CvIQ+Tgoz3IVTzyaGZIrqDdg7VZncwXNPd6b +ze8ixlU56U0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFBjCCAu4CCQCRhyBUzRq1gjANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJU +VzEPMA0GA1UECAwGVGFpd2FuMSUwIwYDVQQKDBxJbm5keSdzIE5ISSBTbWFydGNh +cmQgQ2xpZW50MB4XDTIwMDUwNjE0NTMzM1oXDTIyMDUwNjE0NTMzM1owRTELMAkG +A1UEBhMCVFcxDzANBgNVBAgMBlRhaXdhbjElMCMGA1UECgwcSW5uZHkncyBOSEkg +U21hcnRjYXJkIENsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AK5KShoUDkJ1U8kvzISbDTk9z1QMYGo/jNk3oqL3YS4nhFQt9VSIN8k0Z+ehCKlV +tDQK0sWVUPG8f/lVtKV+MSGMOuQTp0rtUYpTRrbmv/FblF5atI0c3jo0beP47fH7 +1qTYFZep7tR4PFG9U9kDsQCR0nam35ydqq9j/ehBp6/QGAg1gMrhuxj29VefpE9+ +GQXEM41MUIW53ZOlRB8MJkFCfxwfxUbMfuA/JdAkmiRXpM6IpffVfSdhISQPtv6O +ZPs2yHxYp7HOuV7XOxV/34o/A/wiEjoz56eoH+oslgjzB/enk9oBSwkAfYL1FFDN +hWNspkRLEhairRbQuve46l3i7g0jVxUcVIV6PcyC+C9Ci2L9dGX/HBlgmfo8FlX7 +ai2NDujjWGxGp3iFa7wvU/sDZar8ViEUo4FKKgPOY1cRpalBi8uEFXfS9FRh9ckV +rxZDxWsybLwkA+JrfNtKi5XAbJyoHQSG8f7XazuSsh7aLFMNkaHZ1lks3y2nWB+M +z5a89P8Lpj9AXFr/M3LWn9RFg7u+2LCLGFiUmaHG/JfDKxlDTyFh2K+UrgG2c4Ek +9EuehsUVjmJCoMZ1inVDkPixFNuRvlh+TTI0cU+ULVUFtdU5heOuaXXNHV5UDHID +8gucoNJb1aqpNoMtSDNYd5aaxuGwgwMHL/RjwC7lWOj1AgMBAAEwDQYJKoZIhvcN +AQELBQADggIBABibU0zYYGogbwDf+xj/38D9KdpedIW2ZL86ExQyySYgdZm2eOTa +e+qVVIzVOoKQFPtIAXX07M7EDJVIte79eyZnAHWNA0QEgTGBwrltybFyN+PQNpjS ++b+2YsiqB4mtxmkOf4uypT+cUwYBw2Y/yD69lPHtsbYcboud20qzsw28/72sShUY +yfJ9E0BtjsagoTNniUnXx6O3uIN9nzgdC98n9qTRXQqo1YoidsoK+nnY+g0jAYNh +QzyPxinppbM9TGpEWhs1FGaQasTEX/eCbePwPvb5zRKDBplTg1IAATx9njzhhmet +hrYj9elEsWCwV0d7NMp7zbr8pBHAdbvs3DpcPMKpSNA4918Ak89dfRXuKxyvsbRE +e3tr0pLL2NC+9V2C2yj+U/lf+8xUzwBxSbgEjStmwdVPtJj2897iNSCDoKlC3vlY +uaJL0ShEp4FNpdUwa1RB9dSSFeYC/s/FNgk/pcEblE820rP/Y+IMtcmYcTg5c7ta +0g3Scl+JBp0y3fuGhSSZ7iyhqM6RZEKHYTnLbE1mOKx1wAhb0pQ9UC0+EbT/elJs +5EtcLtmgLidA2YKc7u4uJhNvmIIYKE1XQNytsP4PxGjlYJh5C+QzqxwcjfIza+Xc +r/a5yG+yjvxet0lvr04JgHQjvYW6Sqvwn8J+Ioeh8+ZCx/cteEQg9gpl +-----END CERTIFICATE----- diff --git a/certs/host.crt b/certs/host.crt new file mode 100644 index 0000000..1e5e578 --- /dev/null +++ b/certs/host.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFRDCCAyygAwIBAgIJAN6l3sQW0+nHMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAlRXMQ8wDQYDVQQIDAZUYWl3YW4xJTAjBgNVBAoMHElubmR5J3MgTkhJIFNt +YXJ0Y2FyZCBDbGllbnQwHhcNMjAwNTA2MTQ1MzMzWhcNMjIwNTA2MTQ1MzMzWjBE +MQswCQYDVQQGEwJUVzEPMA0GA1UECAwGVGFpd2FuMSQwIgYDVQQKDBtJbm5keXMg +TkhJIFNtYXJ0Y2FyZCBDbGllbnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCufr1KhcY3TcASUwv9RyidgKf9FESn+r97V+QGFpMhewxtAtVnpZIfNr5U +Mvz1sIQ1MmKur2bvcxLrRjfrncJHKmJpBJscZGvViIV4ziIi9bdvpQHVdp4qrLwV +lJOFG5bCirKRuTeC0HRoZ6/1S71NzBwpKsgcoXLmd6PoqUFih6qphoWgN6yo5PQo +/HbHszaVrcTGwdsQBaWzoTihRDeC9lzuRSN4s+3jJvh7H0GRlFsi9suK2KJaQXw9 +BrRGD34gPJMYjaTRTVUyDAI0bW1vsN0GAtKxTfoB3VRCIEbP4DUtOPYI9e0rw2JA +t8oUd4gvaRWTSakOMuBPLssrJaFaQ0CHVrsvuoXJ8gSHPu4pFX0MvPLwXDpvyFWD +Z9B8u7PwIc39Jtq1WAgS/i6+xNQzo7djp+Jzgh8VuMIg1iyzV+jJIhy1WdyoKXl6 +p3cg5DjzF09F3s9Of/3rHpVPnoSwrvKOG/ND2l/TQyEOV9JuStqO27wOZudAd42G +oJX8dn9P/r+MPtOPBFFK1J8jm28aK7KptXN84gV/IH6nEHf9vAcDHy/RaQE9fIYu +pudTWwAhgSdH5i4QcPlpXpQdzSD7PC9uUEdiCHaeze4UWc9N0Qw+oH93bN2MqxsU +Y/9SqfTWVCKQAs4WKF7+C3sB1+gfs0P2/RDuOtHJfSQVQSrlfQIDAQABozgwNjAJ +BgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAcBgNVHREEFTATghFpY2NlcnQubmhpLmdv +di50dzANBgkqhkiG9w0BAQsFAAOCAgEALf5LBL74y9uupYrxYWkzvnWwAnwEVQ1V +g2ygBP7ldrCQIPof69UuWZ2CwN70NwH3AtI6vd2cUBpH2AYyaJe3qYsfsq+/xpdv +FB5xVoIjA3MBuzZWW+PZhtTQ41pzsuKH5MG70SYk/y27/VqQmFTzNBAXLc1130sn +o6uJflYcVE+XxfEj901Zz1BdIcpfu44bqz8ilAaDU6bG4IOBnKQ18GNbfO7Fj+Ei +28UcoGxaiiAcCIektZitaNKjVb4U6uIpNuxhAHxPCZq24z19Iwa4+1eI1GzoxhCW +SFmAZFuF26KVqAOH5Ewo9AlAQ59L99vuhHpyhdt4bXyJCGKYVTJMkEf2oLTaOLoX +1aEpIyCDFY611jJBEEGssajcpZBQWuaK1zTCHUZWp+l1Tme90C5XRPfnCbuei31o +SRK+mDXh1JRWyJxIelta46X3tTN+rEzSnMjUeR1dto8Z54iXu3VctcOfZFrUsKMB +uiwLtN6vYT0jIqlI13C24ybePn6v+1HIc29KNblQVwBpBcU2zcQ6Y2/DAL4NSDBS +hmET24hK/6RpBW+foWxRK6Jl2MSbj+thRBIGKQcAMGs0WOaVk9DNqvOgYGJFixec +HiKJusY06F6ecvC0aaKmbX2kRJf1CvIQ+Tgoz3IVTzyaGZIrqDdg7VZncwXNPd6b +ze8ixlU56U0= +-----END CERTIFICATE----- diff --git a/certs/host.csr b/certs/host.csr new file mode 100644 index 0000000..573c152 --- /dev/null +++ b/certs/host.csr @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIE0DCCArgCAQAwRDELMAkGA1UEBhMCVFcxDzANBgNVBAgMBlRhaXdhbjEkMCIG +A1UECgwbSW5uZHlzIE5ISSBTbWFydGNhcmQgQ2xpZW50MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEArn69SoXGN03AElML/UconYCn/RREp/q/e1fkBhaT +IXsMbQLVZ6WSHza+VDL89bCENTJirq9m73MS60Y3653CRypiaQSbHGRr1YiFeM4i +IvW3b6UB1XaeKqy8FZSThRuWwoqykbk3gtB0aGev9Uu9TcwcKSrIHKFy5nej6KlB +YoeqqYaFoDesqOT0KPx2x7M2la3ExsHbEAWls6E4oUQ3gvZc7kUjeLPt4yb4ex9B +kZRbIvbLitiiWkF8PQa0Rg9+IDyTGI2k0U1VMgwCNG1tb7DdBgLSsU36Ad1UQiBG +z+A1LTj2CPXtK8NiQLfKFHeIL2kVk0mpDjLgTy7LKyWhWkNAh1a7L7qFyfIEhz7u +KRV9DLzy8Fw6b8hVg2fQfLuz8CHN/SbatVgIEv4uvsTUM6O3Y6fic4IfFbjCINYs +s1foySIctVncqCl5eqd3IOQ48xdPRd7PTn/96x6VT56EsK7yjhvzQ9pf00MhDlfS +bkrajtu8DmbnQHeNhqCV/HZ/T/6/jD7TjwRRStSfI5tvGiuyqbVzfOIFfyB+pxB3 +/bwHAx8v0WkBPXyGLqbnU1sAIYEnR+YuEHD5aV6UHc0g+zwvblBHYgh2ns3uFFnP +TdEMPqB/d2zdjKsbFGP/Uqn01lQikALOFihe/gt7AdfoH7ND9v0Q7jrRyX0kFUEq +5X0CAwEAAaBHMEUGCSqGSIb3DQEJDjE4MDYwCQYDVR0TBAIwADALBgNVHQ8EBAMC +BaAwHAYDVR0RBBUwE4IRaWNjZXJ0Lm5oaS5nb3YudHcwDQYJKoZIhvcNAQELBQAD +ggIBAEWsekXqZZ3oLKUfs/WA71gqSD1+cMfL34PNxPbgssJE0t8mj3S9V2NMKDAe +BO/AYY+SjqwmQ8ewhgTAMNH73bDYwUP0qtoyJuh2f1cSDDW9Y8Cm1AqkI1H525AA +eLcNnYoOTZnczVkYROgiK5Sw9iNymxBzuhOK7w09wNRERU1AhtOjrT5cgGCCcxjn +f6sHnvOGThrwi1WWqKT0phA4XH5eLsIFza+etmQLFvkpKeuVkqG4dXeBUgSISrii +yle+TMySa62CfmBmYTIFA0HRSJbEG2C7eUniN6SlAJnfRMTx1uMN4JhOnhCD5Muq +ZhYHX664K6wR9zwu88JS7ob8VSKZoRUJ6v4elBLNCvQTrGKmtBV60LSPmXOt1Git +6H06MaS4DNnrhiPbpkG4uRnPKUy2ZpNZyyokp6VWdipwXy/1KqN7yLw0bqopI8pL +NSsfWw1S6GhwGtxBTXSjNEIzuuLHQL++HQfoSntp2KXMSrSug7xjejrVUUs9a3hR +cZSpZfEXIQwXLr3Gg5ggtcM7QzAxAtaEf28P9eAfTPUhZhO/w6yXJueAs2xaSqIb ++etJpcQG2nr34IwB4HmXZn6yyLx6cShaomzwneFV31rFozpcjhb/Lrz6I7VICDed +MAAKm37arxKjmtKBCunP1OblCzv4rjJ1BqAcnVWIFkthhqvq +-----END CERTIFICATE REQUEST----- diff --git a/certs/host.key b/certs/host.key new file mode 100644 index 0000000..64dc2d6 --- /dev/null +++ b/certs/host.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/certs/san.cnf b/certs/san.cnf new file mode 100644 index 0000000..e67a60a --- /dev/null +++ b/certs/san.cnf @@ -0,0 +1,14 @@ +[ req ] +distinguished_name = req_dn +req_extensions = req_ext +prompt = no +[ req_dn ] +C = TW +ST = Taiwan +O = Inndy's NHI Smartcard Client +[ req_ext ] +basicConstraints = CA:FALSE +keyUsage = digitalSignature, keyEncipherment +subjectAltName = @alt_names +[ alt_names ] +DNS.1 = iccert.nhi.gov.tw diff --git a/certs/trust_ca.cmd b/certs/trust_ca.cmd new file mode 100644 index 0000000..ad8a885 --- /dev/null +++ b/certs/trust_ca.cmd @@ -0,0 +1,10 @@ +@echo off + +net session >nul 2>&1 +if /I %errorLevel% NEQ 0 ( + echo Administrator privilege required + exit +) + +certutil.exe -addstore root ca.crt +pause diff --git a/certs/trust_ca_macos.sh b/certs/trust_ca_macos.sh new file mode 100755 index 0000000..ed7f11c --- /dev/null +++ b/certs/trust_ca_macos.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db ca.crt diff --git a/certs/trust_ca_ubuntu_firefox.sh b/certs/trust_ca_ubuntu_firefox.sh new file mode 100755 index 0000000..6739736 --- /dev/null +++ b/certs/trust_ca_ubuntu_firefox.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [ ! -x "$(which certutil 2>&-)" ] +then + echo "[-] Install libnss3-tools first" +fi + +for f in ~/.mozilla/firefox/*.default*/cert9.db +do + echo -------------------------------------------------------------------------------- + echo $f + certutil -d "${f%/*}" -A -i ca.crt -n 'Inndys NHI Smartcard Client' -t C + certutil -d "${f%/*}" -L +done diff --git a/complicated_sam_hc_auth.py b/complicated_sam_hc_auth.py new file mode 100644 index 0000000..98f5693 --- /dev/null +++ b/complicated_sam_hc_auth.py @@ -0,0 +1,191 @@ +# This file is part of twnhi-smartcard-agent. +# +# twnhi-smartcard-agent is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# twnhi-smartcard-agent is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with twnhi-smartcard-agent. +# If not, see . + +import os +import socket + +from hexdump import hexdump + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa + +from cryptos import DES3, pkcs5_pad, pkcs5_unpad, L_KEY +from errors import ServiceError + +DEBUG = bool(os.getenv('DEBUG_MODE', None)) + +DEFAULT_HOST = os.getenv('NIC_SMARTCARD_AUTH_HOST', 'cloudicap.nhi.gov.tw') +DEFAULT_PORT = int(os.getenv('NIC_SMARTCARD_AUTH_HOST', 443)) + +def recvall(conn, err_code, err_desc): + data = b'' + while not data.endswith(b''): + try: + data += conn.recv(4096) + except Exception as e: + raise ServiceError(err_code, err_desc, e) + + return data + +def send_packet(conn, data, error_code, description): + try: + conn.sendall(data) + except Exception as e: + raise ServiceError(error_code, description, e) + +def encrypt(key, data): + cipher = DES3.new(key, DES3.MODE_ECB) + encrypted = cipher.encrypt(pkcs5_pad(data)) + return encrypted + b'' + +def decrypt(key, data): + # funciton `recvall` already ensures that data will end with b'' + assert data.endswith(b'') + + cipher = DES3.new(key, DES3.MODE_ECB) + decrypted = cipher.decrypt(data[:-3]) + return pkcs5_unpad(decrypted) + +def debug_dump(name, data): + if DEBUG: + print('%s:' % name) + hexdump(data) + +def handshake(conn): + try: + # generate rsa key for handshake + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=1024, + backend=default_backend() + ) + except Exception as e: + raise ServiceError(8300, 'Failed to generate RSA key', e) + + # hello packet, send our public key + try: + pubkey = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).rstrip(b'\n') + except Exception as e: + raise ServiceError(8301, 'Failed to dump public key', e) + + data = b'Hello %s' % pubkey + debug_dump('Local hello', data) + send_packet(conn, encrypt(L_KEY, data), 8306, 'Failed to send handshake packet') + + # recv remote hello + packet = recvall(conn, 8305, 'Recv error') + try: + data = decrypt(L_KEY, packet) + debug_dump('Remote hello', data) + assert data[:5] == b'Hello' + except Exception as e: + raise ServiceError(8305, 'Decrypt error', e) + + # decrypt remote nonce + try: + remote_nonce = private_key.decrypt(data[0x11b:0x11b+0x80], padding.PKCS1v15()) + except Exception as e: + raise ServiceError(8305, 'RSA decrypt error', e) + + # prepare and encrypt our nonce with remote public key + nonce = os.urandom(16) + try: + remote_public = serialization.load_pem_public_key( + data[6:0x116], + backend=default_backend() + ) + enc_nonce = remote_public.encrypt(nonce, padding.PKCS1v15()) + except Exception as e: + raise ServiceError(8303, 'Pubkey encrypt failed', e) + + send_packet(conn, b' %d %s' % (len(enc_nonce), enc_nonce), 8304, \ + 'Failed to send nonce') + + # concat nonces to make session key + return (nonce + remote_nonce)[:24] + +def connect(host=DEFAULT_HOST, port=DEFAULT_PORT): + try: + return socket.create_connection((host, port)) + except Exception as e: + raise ServiceError(4061, 'Can not connect to host', e) + +def sam_hc_auth_check(raise_on_failed=False): + with connect() as conn: + sess_key = handshake(conn) + send_packet(conn, b'77', 8003, 'Failed to send test packet') + ret = recvall(conn, 8005, 'Service check failed') == b'04OK' + + if not ret and raise_on_failed: + raise ServiceError(8005, 'Service check failed') + return ret + +def sam_hc_auth(client, to_sign): + with connect() as conn: + sess_key = handshake(conn) + + # prepare data to be signed + client.select_applet() + hcid = client.get_hc_card_id() + rnd = client.get_random() + + # send auth request + assert len(hcid) == 12 and len(rnd) == 8 + data = b'01%s%s' % (hcid, rnd) + packet = encrypt(sess_key, data) + send_packet(conn, packet, 8003, 'Failed to send auth request 01') + + # recv challenge + packet = recvall(conn, 8005, 'Failed to recv challenge') + data = decrypt(sess_key, packet) + debug_dump('Challenge', data) + # b'02................................' + if not (data.startswith(b'02') and data.endswith(b'')): + raise ServiceError(8005, 'Failed to decrypt challenge') + challenge = data[9:9+32] + + # use hccard to sign challenge + response = client.muauth_hc_dc_sam(challenge) + debug_dump('Response', response) + if len(response) != 16: + raise ServiceError(8006, 'Invalid data length from SAM signing') + + # send challenge and data to be signed + if len(to_sign) != 20: + raise ServiceError(8006, 'Invalid data length `to_sign`') + + data = b'03%s%s' % (response, to_sign) + packet = encrypt(sess_key, data) + send_packet(conn, packet, 8007, 'Failed to send response') + + # got signature + data = decrypt(sess_key, recvall(conn, 8008, 'Failed to recv signature')) + debug_dump('Signature', data) + # b'04OK' ...(256bytes) b'' + if not (data.startswith(b'04OK') and data.endswith(b'')): + raise ServiceError(8008, 'Failed to decrypt signature') + return data[18:-3] + +if __name__ == '__main__': + sam_hc_auth_check() + from hccard import HealthInsuranceSmartcardClient + with HealthInsuranceSmartcardClient() as client: + sig = sam_hc_auth(client, b'00011234123412341234') + print('sig = %r' % sig) diff --git a/cryptos.py b/cryptos.py new file mode 100644 index 0000000..fc84d3d --- /dev/null +++ b/cryptos.py @@ -0,0 +1,121 @@ +# This file is part of twnhi-smartcard-agent. +# +# twnhi-smartcard-agent is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# twnhi-smartcard-agent is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with twnhi-smartcard-agent. +# If not, see . + +import os +import datetime +import hashlib +from Cryptodome.Cipher import DES, DES3 + +bKEY = b'12345678123456780' * 10 +K_BOX = [ + 0x56, 0x28, 0x34, 0x2E, 0x78, 0x5, 0xF, 0x5A, + 0x36, 0x44, 0x42, 0x19, 0x26, 0x95, 0x26, 0x4D, + 0x3, 0x10, 0x15, 0x58, 0x3, 0x40, 0x5A, 0x72, + 0x1E, 0xB, 0x49, 0x69, 0x4B, 0x15, 0x29, 0x6 +] +K_BOX1 = [ + 0x56, 0x28, 0x34, 0x2E, 0x78, 0x5, 0xF, 0x5A, + 0x36, 0x44, 0x42, 0x19, 0x26, 0x95, 0x26, 0x4D, + 0x2D, 0x41, 0x4D, 0x1F, 0x41, 0x62, 0x15, 0x2F +] + +L_KEY = bytes(bKEY[K_BOX[i]] for i in range(16)) + b'\0' * 8 +L_KEY1 = bytes(bKEY[K_BOX1[i]] for i in range(24)) + +KEY_SUFFIX = b'\x27\x06\x58\x66' + +TDesLKey = DES3.new(L_KEY, DES3.MODE_ECB) +TDesLKey1 = DES3.new(L_KEY1, DES3.MODE_ECB) + +def iv_pad(d): + def rand_byte(): + return os.urandom(1) + bcount = len(d) // 7 + if len(d) % 7: + bcount += 1 + + blocks = [ d[i*7:i*7 + 7].ljust(7, b'\0') + rand_byte() for i in range(bcount) ] + return b''.join(blocks) + +def iv_remove(d, flag=True): + c = b''.join(d[i*8:i*8+7] for i in range(len(d) // 8)) + if flag: + unpad_size = (len(c) // 8) * 8 + return c[:unpad_size] + return c + +def pkcs5_tail(n): + return bytes([n]) * n + +def pkcs5_pad(data): + padding_size = 8 - len(data) % 8 + return data + pkcs5_tail(padding_size) + +def pkcs5_unpad(data): + last_byte = data[-1] + tail = data[-last_byte:] + if last_byte > 8 or bytes(tail) != pkcs5_tail(last_byte): + raise ValueError('Inalid PKCS5 padding') + return data[:-last_byte] + +def card_encrypt(data, cardid): + t = datetime.date.today().strftime('%Y%m%d') + tdeskey = hashlib.sha1((cardid + t).encode('ascii')).digest() + KEY_SUFFIX + cipher = DES3.new(tdeskey, DES3.MODE_ECB) + + data = cipher.encrypt(pkcs5_pad(data)) + return TDesLKey1.encrypt(iv_pad(data)) + +def card_decrypt(data, cardid): + t = datetime.date.today().strftime('%Y%m%d') + tdeskey = hashlib.sha1((cardid + t).encode('ascii')).digest() + KEY_SUFFIX + cipher = DES3.new(tdeskey, DES3.MODE_ECB) + + data = TDesLKey1.decrypt(data) + data = iv_remove(data) + data = cipher.decrypt(data) + return pkcs5_unpad(data) + +def basic_encrypt(data): + key = datetime.date.today().strftime('%m%d%Y').encode('ascii') + cipher = DES.new(key, DES.MODE_ECB) + return cipher.encrypt(iv_pad(data)) + +def basic_decrypt(data): + key = datetime.date.today().strftime('%m%d%Y').encode('ascii') + cipher = DES.new(key, DES.MODE_ECB) + decrypted = cipher.decrypt(data) + return iv_remove(decrypted, False) + +if __name__ == '__main__': + data, card_id = b'123456123456', '000000000001' + if card_decrypt(card_encrypt(data, card_id), card_id) == data: + print('card_* : Pass') + else: + print('card_* : Failed') + + for i in range(40, 40+2*6): + test_data = bytes(range(i)) + if basic_decrypt(basic_encrypt(test_data + b'\xff')).split(b'\xff')[0] != test_data: + print('basic_* : Failed') + break + else: + print('basic_* : Pass') + + import sys + if len(sys.argv) > 1: + data = bytes.fromhex(sys.argv[1]) + print(basic_decrypt(data).decode('big5-hkscs')) diff --git a/docs/chrome-foxyproxy-01.png b/docs/chrome-foxyproxy-01.png new file mode 100644 index 0000000..547f76a Binary files /dev/null and b/docs/chrome-foxyproxy-01.png differ diff --git a/docs/chrome-foxyproxy-02.png b/docs/chrome-foxyproxy-02.png new file mode 100644 index 0000000..68cd5d9 Binary files /dev/null and b/docs/chrome-foxyproxy-02.png differ diff --git a/docs/chrome-foxyproxy-03.png b/docs/chrome-foxyproxy-03.png new file mode 100644 index 0000000..13f8f30 Binary files /dev/null and b/docs/chrome-foxyproxy-03.png differ diff --git a/docs/chrome-foxyproxy-04.png b/docs/chrome-foxyproxy-04.png new file mode 100644 index 0000000..135709d Binary files /dev/null and b/docs/chrome-foxyproxy-04.png differ diff --git a/docs/chrome-foxyproxy-05.png b/docs/chrome-foxyproxy-05.png new file mode 100644 index 0000000..402d20c Binary files /dev/null and b/docs/chrome-foxyproxy-05.png differ diff --git a/docs/setup-socks5-proxy-chrome-foxyproxy.md b/docs/setup-socks5-proxy-chrome-foxyproxy.md new file mode 100644 index 0000000..90b58d8 --- /dev/null +++ b/docs/setup-socks5-proxy-chrome-foxyproxy.md @@ -0,0 +1,23 @@ +# Chrome FoxyProxy 使用說明 / Chrome FoxyProxy Usage + +## 新增 Proxy / Config new proxy + +### 開啟 FoxyProxy 設定 / Open "options" of FoxyProxy + +![開啟 FoxyProxy 設定 / Open "options" of FoxyProxy](chrome-foxyproxy-01.png) + +### 按下 "Add New Proxy" / Click "Add New Proxy" + +![按下 "Add New Proxy" / Click "Add New Proxy"](chrome-foxyproxy-02.png) + +### 填寫 Proxy 資料並且按下 "Save" / Fill proxy config and click "Save" + +![填寫 Proxy 資料並且按下 "Save" / Fill proxy config and click "Save"](chrome-foxyproxy-03.png) + +## 啟用 Proxy / Enable Proxy + +![啟用 Proxy / Enable Proxy](chrome-foxyproxy-04.png) + +## 停用 Proxy / Disable Proxy + +![停用 Proxy / Disable Proxy](chrome-foxyproxy-04.png) \ No newline at end of file diff --git a/errors.py b/errors.py new file mode 100644 index 0000000..c7262c9 --- /dev/null +++ b/errors.py @@ -0,0 +1,21 @@ +# This file is part of twnhi-smartcard-agent. +# +# twnhi-smartcard-agent is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# twnhi-smartcard-agent is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with twnhi-smartcard-agent. +# If not, see . + +class ServiceError(Exception): + def __init__(self, error_code, description, *args): + super().__init__(*args) + self.error_code = error_code + self.description = description diff --git a/hccard.py b/hccard.py new file mode 100644 index 0000000..33e1899 --- /dev/null +++ b/hccard.py @@ -0,0 +1,170 @@ +# This file is part of twnhi-smartcard-agent. +# +# twnhi-smartcard-agent is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# twnhi-smartcard-agent is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with twnhi-smartcard-agent. +# If not, see . + +#!/usr/bin/env python3 +import logging +import sys +from collections import namedtuple + +from smartcard.System import readers as get_readers +from smartcard.util import toHexString + +logging.basicConfig(level='INFO', stream=sys.stdout) +logger = logging.getLogger(__name__) + +class SmartcardException(Exception): + pass + +class SmartcardCommandException(SmartcardException): + def __init__(self, *args): + super.__init__(*args) + self.error_code = None + self.description = None + +class SmartcardClient: + def __init__(self, conn=None): + if conn is None: + conn = select_reader_and_connect() + + if not conn: + raise SmartcardException('Smartcard connection was not provided') + self.conn = conn + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + if self.conn: + self.conn.disconnect() + + def fire(self, cmd): + data, a, b = self.conn.transmit(cmd) + if (a, b) != (0x90, 0x00): + raise SmartcardCommandException(data, (a, b)) + return bytes(data) + +HCBasicData = namedtuple('HCBaseData', ['card_id', 'id', 'name', 'birth', 'gender', 'unknown']) + +def error_info(error_code, description): + def error_wrapper(f): + def wrapper(*args, **kwargs): + try: + return f(*args, **kwargs) + except SmartcardCommandException as e: + e.error_code = error_code + e.description = description + raise + return wrapper + return error_wrapper + +class HealthInsuranceSmartcardClient(SmartcardClient): + @error_info(7004, 'Failed to select applet') + def select_applet(self): + logger.debug('select default applet') + self.fire([ + 0x00, 0xA4, 0x04, 0x00, 0x10, 0xD1, 0x58, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x11, 0x00 + ]) + + def select_sam_applet(self): + logger.debug('select sam applet') + self.fire([ + 0x00, 0xA4, 0x04, 0x00, 0x10, 0xD1, 0x58, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x31, 0x00 + ]) + + @error_info(8011, 'Failed to get basic data') + def get_basic(self): + logger.debug('get basic data') + data = self.fire([0, 0xca, 0x11, 0, 2, 0, 0, 0]) + return HCBasicData( + data[:12].decode('ascii'), + data[32:42].decode('ascii'), + data[12:32].rstrip(b'\0').decode('big5-hkscs'), + data[42:49].decode('ascii'), + data[49:50].decode('ascii'), + data[50:].decode('ascii'), + ) + + @error_info(8010, 'Failed to get card data') + def get_hc_card_data(self): + logger.debug('get HC card data') + return self.fire([0, 0xca, 0x24, 0, 2, 0, 0, 0]) + + @error_info(8001, 'Failed to get card id') + def get_hc_card_id(self): + logger.debug('get HC card id') + return self.fire([0, 0xca, 0, 0, 2, 0, 0, 0]) + + @error_info(8002, 'Failed to get card random') + def get_random(self): + logger.debug('get random') + return self.fire([0, 0x84, 0, 0, 8]) + + @error_info(8006, 'Secure access module signing failed') + def muauth_hc_dc_sam(self, data: bytes): + logger.debug('muauth_hc_dc_sam') + if len(data) > 32: + raise ValueError('data size must be less than 33 bytes') + + prefix = [0x00, 0x82, 0x11, 0x12, 0x20] + suffix = [0x10] + + payload = prefix + list(data.ljust(32, b'\0')) + suffix + assert len(payload) == 0x26 + return self.fire(payload) + +def select_reader_and_connect(interactive=False): + readers = get_readers() + + if not readers: + logger.error('Please connect your smartcard reader') + return + elif len(readers) == 1: + logger.info('Only one reader connected, use that one: %s', readers[0]) + reader = readers[0] + elif not interactive: + logger.info('Non-interactive was used, select first reader') + reader = readers[0] + else: + print('%d readers available, please select one:' % len(readers)) + for i, r in enumerate(readers): + print('%-2d : %s' % (i, r)) + + idx = int(input('\n Reader number: ')) + reader = readers[idx] + + conn = reader.createConnection() + conn.connect() + return conn + +if __name__ == '__main__': + try: + conn = select_reader_and_connect(True) + if not conn: + raise Exception('No reader connected or selection failed') + except Exception as e: + logger.exception('Can not connect to reader, error: %r', e) + sys.exit(1) + + with HealthInsuranceSmartcardClient(conn) as client: + client.select_applet() + print(client.get_basic()) diff --git a/install-packages.py b/install-packages.py new file mode 100644 index 0000000..5c2634a --- /dev/null +++ b/install-packages.py @@ -0,0 +1,144 @@ +# This file is part of twnhi-smartcard-agent. +# +# twnhi-smartcard-agent is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# twnhi-smartcard-agent is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with twnhi-smartcard-agent. +# If not, see . + +import os +import sys + +from hashlib import sha256 +from imp import reload +from io import BytesIO +from pprint import pprint +from urllib.request import urlopen +from zipfile import ZipFile + +SWIG_LOCAL_FILENAME = 'swigwin-4.0.1.zip' +SIWG_ZIP_URL = 'http://prdownloads.sourceforge.net/swig/swigwin-4.0.1.zip' +SWIG_ZIP_HASH = '8c504241ad4fb4f8ba7828deaef1ea0b4972e86eb128b46cb75efabf19ab4745' + +is_windows = os.name == 'nt' + +def pyexec(*args, executable=sys.executable): + return os.system('%s -m %s' % (executable, ' '.join(args))) + +def which(fname): + if is_windows: + fname += '.exe' + + for p in os.getenv('PATH').split(os.path.pathsep): + full = os.path.join(p, fname) + if os.path.exists(full): + return full + +def check_version(): + if sys.version_info.major < 3 or \ + sys.version_info.minor < 6: + print('[-] Python version not match: %s' % sys.version) + exit() + +def install_virtualenv(): + try: + import virtualenv + major_version = int(virtualenv.__version__.split('.')[0]) + if major_version >= 20: + return + else: + print('[*] Upgrade virtualenv') + except: + pass + + ret = pyexec('pip', 'install', '-U', '--user', 'virtualenv') + if ret: + print('[-] Failed to execute pip') + exit(1) + +def load_virtualenv(): + if not os.path.exists('venv'): + print('[*] Create new virtualenv') + pyexec('virtualenv', '--copies', '--download', 'venv') + + print('[*] Activate venv in current interpreter') + the_file = os.path.join('venv', 'Scripts', 'activate_this.py') \ + if is_windows else \ + os.path.join('venv', 'bin', 'activate_this.py') + exec(open(the_file).read(), {'__file__': the_file}) + +def load_swig(): + if not is_windows: + return + + if which('swig'): + return + + if not os.path.exists(SWIG_LOCAL_FILENAME): + print('[+] Downloading file from %s' % SIWG_ZIP_URL) + response = urlopen(SIWG_ZIP_URL) + data = response.read() + + with open(SWIG_LOCAL_FILENAME, 'wb') as fp: + fp.write(data) + else: + print('[+] Use %s from local' % SWIG_LOCAL_FILENAME) + with open(SWIG_LOCAL_FILENAME, 'rb') as fp: + data = fp.read() + + print('[*] Check if file hash match %s' % SWIG_ZIP_HASH) + assert sha256(data).hexdigest().lower() == SWIG_ZIP_HASH + + print('[*] Read zip file') + zfile = ZipFile(BytesIO(data)) + pathname = zfile.infolist()[0].filename + if os.path.exists(pathname): + print('[+] Zip file already extracted') + else: + print('[+] Extracting files') + zfile.extractall('.') + + path = os.getenv('PATH') + swig_path = os.path.join(os.path.abspath('.'), 'swigwin-4.0.1') + new_path = swig_path + os.path.pathsep + path + os.putenv('PATH', new_path) + print('New $PATH:') + pprint(new_path.split(os.path.pathsep)) + +def install_dependencies(): + print('[*] Installing dependencies') + ret = pyexec('pip', 'install', '-r', 'requirements.txt', executable='python') + if ret: + print('[-] Failed to install dependencies') + exit(1) + +def try_import_packages(): + try: + import hexdump + import websockets + import Cryptodome + import smartcard + except ImportError as e: + print('[-] Can not import one of dependencies: %s' % e.name) + exit(1) + +def finish(): + print('[!] We are good to go!') + print('[*] Follow post-installation instructions to setup root certiciate and run the program') + +if __name__ == '__main__': + check_version() + install_virtualenv() + load_virtualenv() + load_swig() + install_dependencies() + try_import_packages() + finish() diff --git a/pysoxy.py b/pysoxy.py new file mode 100644 index 0000000..79b7ae5 --- /dev/null +++ b/pysoxy.py @@ -0,0 +1,357 @@ +# This file is part of twnhi-smartcard-agent. +# +# twnhi-smartcard-agent is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# twnhi-smartcard-agent is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with twnhi-smartcard-agent. +# If not, see . + +# -*- coding: utf-8 -*- +""" + Small Socks5 Proxy Server in Python + from https://github.com/MisterDaneel/ +""" + +# Network +import socket +import select +from struct import pack, unpack +# System +import traceback +from threading import Thread, activeCount +from signal import signal, SIGINT, SIGTERM +from time import sleep +import sys + +hijacker = None +hijacked_host = None + +# +# Configuration +# +MAX_THREADS = 200 +BUFSIZE = 2048 +TIMEOUT_SOCKET = 5 +LOCAL_ADDR = '127.0.0.1' +LOCAL_PORT = 17777 +# Parameter to bind a socket to a device, using SO_BINDTODEVICE +# Only root can set this option +# If the name is an empty string or None, the interface is chosen when +# a routing decision is made +# OUTGOING_INTERFACE = "eth0" +OUTGOING_INTERFACE = "" + +# +# Constants +# +'''Version of the protocol''' +# PROTOCOL VERSION 5 +VER = b'\x05' +'''Method constants''' +# '00' NO AUTHENTICATION REQUIRED +M_NOAUTH = b'\x00' +# 'FF' NO ACCEPTABLE METHODS +M_NOTAVAILABLE = b'\xff' +'''Command constants''' +# CONNECT '01' +CMD_CONNECT = b'\x01' +'''Address type constants''' +# IP V4 address '01' +ATYP_IPV4 = b'\x01' +# DOMAINNAME '03' +ATYP_DOMAINNAME = b'\x03' + + +class ExitStatus: + """ Manage exit status """ + def __init__(self): + self.exit = False + + def set_status(self, status): + """ set exist status """ + self.exit = status + + def get_status(self): + """ get exit status """ + return self.exit + + +def error(msg="", err=None): + """ Print exception stack trace python """ + if msg: + traceback.print_exc() + print("[-] {} - Code: {}, Message: {}".format(msg, str(err[0]), err[1])) + else: + traceback.print_exc() + + +def proxy_loop(socket_src, socket_dst): + """ Wait for network activity """ + while not EXIT.get_status(): + try: + reader, _, _ = select.select([socket_src, socket_dst], [], [], 1) + except select.error as err: + error("Select failed", err) + return + if not reader: + continue + try: + for sock in reader: + data = sock.recv(BUFSIZE) + if not data: + return + if sock is socket_dst: + socket_src.send(data) + else: + socket_dst.send(data) + except socket.error as err: + error("Loop failed", err) + return + + +def connect_to_dst(dst_addr, dst_port): + """ Connect to desired destination """ + sock = create_socket() + if OUTGOING_INTERFACE: + try: + sock.setsockopt( + socket.SOL_SOCKET, + socket.SO_BINDTODEVICE, + OUTGOING_INTERFACE.encode(), + ) + except PermissionError as err: + print("[-] Only root can set OUTGOING_INTERFACE parameter") + EXIT.set_status(True) + try: + sock.connect((dst_addr, dst_port)) + print('[+] Connect to %s:%d' % (dst_addr, dst_port)) + return sock + except socket.error as err: + error("Failed to connect to DST", err) + return 0 + + +def request_client(wrapper): + """ Client request details """ + # +----+-----+-------+------+----------+----------+ + # |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + # +----+-----+-------+------+----------+----------+ + try: + s5_request = wrapper.recv(BUFSIZE) + except ConnectionResetError: + if wrapper != 0: + wrapper.close() + error() + return False + # Check VER, CMD and RSV + if ( + s5_request[0:1] != VER or + s5_request[1:2] != CMD_CONNECT or + s5_request[2:3] != b'\x00' + ): + return False + # IPV4 + if s5_request[3:4] == ATYP_IPV4: + dst_addr = socket.inet_ntoa(s5_request[4:-2]) + dst_port = unpack('>H', s5_request[8:len(s5_request)])[0] + # DOMAIN NAME + elif s5_request[3:4] == ATYP_DOMAINNAME: + sz_domain_name = s5_request[4] + dst_addr = s5_request[5: 5 + sz_domain_name - len(s5_request)] + port_to_unpack = s5_request[5 + sz_domain_name:len(s5_request)] + dst_port = unpack('>H', port_to_unpack)[0] + else: + return False + return (dst_addr, dst_port) + + +def request(wrapper): + """ + The SOCKS request information is sent by the client as soon as it has + established a connection to the SOCKS server, and completed the + authentication negotiations. The server evaluates the request, and + returns a reply + """ + dst = request_client(wrapper) + # Server Reply + # +----+-----+-------+------+----------+----------+ + # |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + # +----+-----+-------+------+----------+----------+ + rep = b'\x07' + bnd = b'\x00' + b'\x00' + b'\x00' + b'\x00' + b'\x00' + b'\x00' + hijacked = False + if dst: + if dst[0] == hijacked_host.encode(): + print('[*] Hijack %s to local server' % hijacked_host) + hijacked = True + socket_dst = True + else: + socket_dst = connect_to_dst(dst[0], dst[1]) + + if not dst or socket_dst == 0: + rep = b'\x01' + else: + rep = b'\x00' + if hijacked: + bnd = b'\x01\x01\x01\x01\x01\x01' + else: + bnd = socket.inet_aton(socket_dst.getsockname()[0]) + bnd += pack(">H", socket_dst.getsockname()[1]) + + reply = VER + rep + b'\x00' + ATYP_IPV4 + bnd + try: + wrapper.sendall(reply) + except socket.error: + if wrapper != 0: + wrapper.close() + return + # start proxy + if rep == b'\x00': + if hijacked: + hijacker(wrapper) + else: + proxy_loop(wrapper, socket_dst) + if wrapper != 0: + wrapper.close() + if socket_dst != 0 and socket_dst != True: + socket_dst.close() + + +def subnegotiation_client(wrapper): + """ + The client connects to the server, and sends a version + identifier/method selection message + """ + # Client Version identifier/method selection message + # +----+----------+----------+ + # |VER | NMETHODS | METHODS | + # +----+----------+----------+ + try: + identification_packet = wrapper.recv(BUFSIZE) + except socket.error: + error() + return M_NOTAVAILABLE + # VER field + if VER != identification_packet[0:1]: + return M_NOTAVAILABLE + # METHODS fields + nmethods = identification_packet[1] + methods = identification_packet[2:] + if len(methods) != nmethods: + return M_NOTAVAILABLE + for method in methods: + if method == ord(M_NOAUTH): + return M_NOAUTH + return M_NOTAVAILABLE + + +def subnegotiation(wrapper): + """ + The client connects to the server, and sends a version + identifier/method selection message + The server selects from one of the methods given in METHODS, and + sends a METHOD selection message + """ + method = subnegotiation_client(wrapper) + # Server Method selection message + # +----+--------+ + # |VER | METHOD | + # +----+--------+ + if method != M_NOAUTH: + return False + reply = VER + method + try: + wrapper.sendall(reply) + except socket.error: + error() + return False + return True + + +def connection(wrapper): + """ Function run by a thread """ + if subnegotiation(wrapper): + request(wrapper) + + +def create_socket(): + """ Create an INET, STREAMing socket """ + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(TIMEOUT_SOCKET) + except socket.error as err: + error("Failed to create socket", err) + sys.exit(0) + return sock + + +def bind_port(sock): + """ + Bind the socket to address and + listen for connections made to the socket + """ + try: + print('[+] Socks5 proxy bind on {}:{}'.format(LOCAL_ADDR, str(LOCAL_PORT))) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((LOCAL_ADDR, LOCAL_PORT)) + except socket.error as err: + error("Bind failed", err) + sock.close() + sys.exit(0) + # Listen + try: + sock.listen(10) + except socket.error as err: + error("Listen failed", err) + sock.close() + sys.exit(0) + return sock + + +def exit_handler(signum, frame): + """ Signal handler called with signal, exit script """ + print('[*] Signal handler called with signal', signum) + EXIT.set_status(True) + + +def main(hijack, host): + """ Main function """ + global hijacker + global hijacked_host + hijacker = hijack + hijacked_host = host + new_socket = create_socket() + bind_port(new_socket) + #signal(SIGINT, exit_handler) + #signal(SIGTERM, exit_handler) + while not EXIT.get_status(): + if activeCount() > MAX_THREADS: + sleep(3) + continue + try: + wrapper, _ = new_socket.accept() + wrapper.setblocking(1) + except socket.timeout: + continue + except socket.error: + error() + continue + except TypeError: + error() + sys.exit(0) + recv_thread = Thread(target=connection, args=(wrapper, )) + recv_thread.start() + new_socket.close() + + +EXIT = ExitStatus() +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f991f46 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +cryptography +hexdump +pycryptodomex +pyscard +websockets diff --git a/server.py b/server.py new file mode 100644 index 0000000..e465d76 --- /dev/null +++ b/server.py @@ -0,0 +1,199 @@ +# This file is part of twnhi-smartcard-agent. +# +# twnhi-smartcard-agent is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# twnhi-smartcard-agent is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with twnhi-smartcard-agent. +# If not, see . + +#!/usr/bin/env python3 +import asyncio +import atexit +import contextlib +import http +import logging +import os +import ssl +import subprocess +import sys +import threading + +import websockets +from hccard import HealthInsuranceSmartcardClient, select_reader_and_connect, \ + SmartcardCommandException +from cryptos import card_encrypt, basic_encrypt +from complicated_sam_hc_auth import sam_hc_auth, sam_hc_auth_check +from errors import ServiceError + +logging.basicConfig(level='INFO', stream=sys.stdout) +logger = logging.getLogger('server') + +HOST = 'iccert.nhi.gov.tw' +CENSORED_COMMANDS = ['EnCrypt', 'SecureGetBasicWithParam', 'GetBasic'] + +lock = threading.Lock() + +class HTTP(websockets.WebSocketServerProtocol): + async def process_request(self, path, request_headers): + if path == '/echo': + return await super().process_request(path, request_headers) + elif path == '/exit': + exit() + elif path == '/': + body = b'It works!\n' + return http.HTTPStatus.OK, [('Content-Length', str(len(body)))], body + else: + return http.HTTPStatus.NOT_FOUND, [], b'' + + @staticmethod + def process_origin(headers, origins): + origin = websockets.WebSocketServerProtocol.process_origin(headers, origins) + + if origin: + print('[*] wss connection from: %s' % origin) + + if not origin or not origin.endswith('.gov.tw') and \ + not origin.endswith('iccert.nhi.gov.tw:7777'): + raise websockets.InvalidOrigin(origin) + + + return origin + +def connect_reader(): + try: + return HealthInsuranceSmartcardClient() + except: + raise ServiceError(8013, 'Can not connect to smartcard reader') + +def get_basic_data(): + with lock, connect_reader() as client: + try: + client.select_applet() + data = list(client.get_basic()[:-1]) + data.append(client.get_hc_card_data().decode('ascii')[:1]) + return ','.join(data) + except SmartcardCommandException as e: + raise + except: + raise ServiceError(8011, 'Failed to read basic data from smartcard') + +def get_basic_data_encrypted(password): + # Yes, password was not used to encrypt the data! + # maybe we should remove the password argument and rename it to encoded? + blob = get_basic_data().encode('big5-hkscs') + return basic_encrypt(blob).hex().upper() + +async def handler(ws, path): + try: + while True: + cmd = await ws.recv() + log_censored = any(cmd.startswith(c) for c in CENSORED_COMMANDS) + def censor_data(data, splitter='='): + if log_censored: + data_list = data.split(splitter, maxsplit=1) + if len(data_list) == 1: + return data + else: + return data_list[0] + splitter + '...(censored)' + return data + logger.info('InCmd = {{{ %s }}}' % censor_data(cmd)) + prefix = '' + + try: + if cmd == 'Exit': + exit() + + elif cmd == 'GetVersion': + ret = 'GetVersion:0001' + + elif cmd == 'GetBasic': + prefix = 'GetBasic:' + ret = get_basic_data() + + elif cmd == 'GetRandom': + rnd = int.from_bytes(os.urandom(8), 'little') + ret = str(rnd).zfill(16)[-16:] + assert len(ret) == 16 + ret = 'GetRandom:%s' % ret + + elif cmd.startswith('EnCrypt?Pwd='): + prefix = 'EnCrypt:' + data = cmd.split('=', maxsplit=1)[1].encode('ascii') + + if not (6 <= len(data) <= 12): + raise ServiceError(8009, 'Invalid password length (6 <= len <= 12)') + + with lock, connect_reader() as client: + client.select_applet() + card_id = client.get_hc_card_id().decode('ascii') + + encrypted = card_encrypt(data, card_id) + ret = encrypted.hex().upper() + + elif cmd.startswith('H_Sign?Random='): + prefix = 'H_Sign:' + data = cmd.split('=')[1].encode('ascii') + assert len(data) == 20 and data[:4] == b'0001' + sam_hc_auth_check(raise_on_failed=True) + with lock, connect_reader() as client: + sig = sam_hc_auth(client, data) + ret = sig.decode('ascii') + + elif cmd.startswith('SecureGetBasicWithParam?Pwd='): + prefix = 'SecureGetBasicWithParam:' + pwd = cmd.split('=', maxsplit=1)[0] + ret = get_basic_data_encrypted(pwd) + + else: + ret = '9999' + except (SmartcardCommandException, ServiceError) as e: + if isinstance(e.error_code, (int, str)): + prefix = '' + result = '%d' % e.error_code + logger.error('Error = {{{ %d: %s }}}' % (e.error_code, e.description)) + else: + result = '9876' + logger.error('Error = {{{ Unexpected Error -> %r }}}' % e) + else: + result = prefix + ret + + await ws.send(result) + + result = censor_data(result, ':') + if len(result) >= 32: + result = '%s...(%d bytes)' % (result[:32], len(result) - 32) + logger.info('OutResult = {{{ %s }}}' % result) + except websockets.ConnectionClosedOK: + pass + except websockets.ConnectionClosedError: + pass + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +ssl_context.load_cert_chain('certs/chain.crt', 'certs/host.key') + +import pysoxy + +class PolyServer: + def is_serving(self): + return True + +def forwarder(sock): + # this function will be executed in new thread, + # we need to create a new event loop + event_loop = asyncio.new_event_loop() + + server = websockets.WebSocketServer(event_loop) + server.wrap(PolyServer()) + + _, conn = event_loop.run_until_complete(event_loop.connect_accepted_socket(lambda: HTTP(handler, server, host='localhost', port=7777, secure=True), sock, ssl=ssl_context)) + event_loop.run_until_complete(conn.wait_closed()) + +pysoxy.main(forwarder, HOST)