mirror of
https://github.com/Inndy/twnhi-smartcard-agent.git
synced 2025-07-17 12:43:21 +00:00
First public release
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
/venv
|
||||
/swigwin-4.0.1
|
||||
/swigwin-4.0.1.zip
|
15
LICENSE
Normal file
15
LICENSE
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
229
README.md
Normal file
229
README.md
Normal file
@ -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)
|
7
certs/.gitignore
vendored
Normal file
7
certs/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
*.crt
|
||||
*.key
|
||||
ca.srl
|
||||
!ca.crt
|
||||
!chain.crt
|
||||
!host.crt
|
||||
!host.key
|
36
certs/Makefile
Normal file
36
certs/Makefile
Normal file
@ -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
|
29
certs/ca.crt
Normal file
29
certs/ca.crt
Normal file
@ -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-----
|
60
certs/chain.crt
Normal file
60
certs/chain.crt
Normal file
@ -0,0 +1,60 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
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-----
|
31
certs/host.crt
Normal file
31
certs/host.crt
Normal file
@ -0,0 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
ze8ixlU56U0=
|
||||
-----END CERTIFICATE-----
|
28
certs/host.csr
Normal file
28
certs/host.csr
Normal file
@ -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-----
|
51
certs/host.key
Normal file
51
certs/host.key
Normal file
@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKgIBAAKCAgEArn69SoXGN03AElML/UconYCn/RREp/q/e1fkBhaTIXsMbQLV
|
||||
Z6WSHza+VDL89bCENTJirq9m73MS60Y3653CRypiaQSbHGRr1YiFeM4iIvW3b6UB
|
||||
1XaeKqy8FZSThRuWwoqykbk3gtB0aGev9Uu9TcwcKSrIHKFy5nej6KlBYoeqqYaF
|
||||
oDesqOT0KPx2x7M2la3ExsHbEAWls6E4oUQ3gvZc7kUjeLPt4yb4ex9BkZRbIvbL
|
||||
itiiWkF8PQa0Rg9+IDyTGI2k0U1VMgwCNG1tb7DdBgLSsU36Ad1UQiBGz+A1LTj2
|
||||
CPXtK8NiQLfKFHeIL2kVk0mpDjLgTy7LKyWhWkNAh1a7L7qFyfIEhz7uKRV9DLzy
|
||||
8Fw6b8hVg2fQfLuz8CHN/SbatVgIEv4uvsTUM6O3Y6fic4IfFbjCINYss1foySIc
|
||||
tVncqCl5eqd3IOQ48xdPRd7PTn/96x6VT56EsK7yjhvzQ9pf00MhDlfSbkrajtu8
|
||||
DmbnQHeNhqCV/HZ/T/6/jD7TjwRRStSfI5tvGiuyqbVzfOIFfyB+pxB3/bwHAx8v
|
||||
0WkBPXyGLqbnU1sAIYEnR+YuEHD5aV6UHc0g+zwvblBHYgh2ns3uFFnPTdEMPqB/
|
||||
d2zdjKsbFGP/Uqn01lQikALOFihe/gt7AdfoH7ND9v0Q7jrRyX0kFUEq5X0CAwEA
|
||||
AQKCAgEAq2a1G1myLarCy30l3sGiJKw21wKsufA1XLwlsNFF7vJGb2IEK85YbS7B
|
||||
4EVBczjTdMmsY3jJ/NUlNVQBJAEP0AXTKuMqVcZSoip7KQIaSArjB9imp37fuH16
|
||||
Nxx9l5dVDH1fEINGAsouPkvzbFjcd2nSE6IBdRYlnjrRF34CSv2GZwVLhuiJQlG7
|
||||
f/MV3e2s5XQOQUo0m1VgwcTQsqAmgw7qk+X4BN2BA8rI82/tYUnAB+UyZI2NVGjU
|
||||
18EZHWSkeJfnyYuA5VM4J3PiSotenwK06O2m9iDpPiGhXV8FD7Zlpak5C+497On8
|
||||
PiQKbPZJIIDxf38wf1D8QuttCFHrXfZJvbXFWXY4df41yJmMXeF1Dk/6JRlROcSW
|
||||
jjM3ngW2YTABbGq7CxFr/c8h9u5wQ5vdDEuEmnMrk6I6QgFA01jqdW8cRnx4U5nH
|
||||
hxWEhh1TiOAYo8k+Pa9jvZIJAjgQ8cAJDuhBMsFZuSacnuZLWgO/r6FeMW4ECckc
|
||||
iSKnW/4+oYK8nyONXKTni0RgHHXmV+AfV7cRbBj/e6ca8CqMQ0ZJBCImyvWn+VaN
|
||||
UVz2r7klz58MxaWz3IDdJhmn7ppO78/9uaY7TIkmUR5rOY4BapLzCVEqwDPLYJTj
|
||||
PoO4qr1jtMcCrM10NWeshRTF1zvdrWzhctGYtb5/KOdcI/rGbgECggEBANV8sQ3/
|
||||
/s2ANFl0iF3KpUvIUNft9t0a7wp8wBrWSPuq/ZyGUKTDhceZd204D0R62GyDBtNC
|
||||
UD/DIQ/kPCoB3sXr5U9LEG7RF1SZjZHTy11EP1hzKBP+GQCd00Ab3aTKFRweq9Qp
|
||||
AUaXhu2W3iyV1Qs7RK6AtQR2BAyMWM2zX9ITYb9D5NXEndW247YDOgM55LUBQgxW
|
||||
DezgFBnOiIOvKjcLVlbAOBBmU82xpRqks6a1I6BYl2KTeHUCgxdZAZ2qjMVIFo8J
|
||||
QVteTWcJqZFS1jlKdCa50LeKt8pBNjKhVCyQWUcrVyhylpEvSHdfL6XVWjo+GPxt
|
||||
wIcGsvhjMOkEw/0CggEBANE+SJmpKOsjHU5bsNv5Hxk9f15+bDTjg5j8RIhKYSB/
|
||||
Aw/pyUp4ewMiv0vhnN5x5nqonuvKS3r+yEzi2vGIlis/IuldLiOeUlJGgnQAr0EH
|
||||
7sHbMLW6isV5lH7ma/JPzmVVwleiAP/T0hbN5r9YCWAIlE1ZLd7kKdSOjbuoyLzL
|
||||
2YAFNV2s0Pg/taPKiTQMKYkhz5oYLPJbLzbX68kS0UIubYlv9uRm+yucyc1h6ewM
|
||||
UN1NZnhT8mI29+A7+NXnvDF8/s/quq8YiiN3SOB8Vmz5JkTdpSe8IAZjRvaNEok/
|
||||
0I/8+iPQoO+HbqkxhZsEzdZmeW581nsFuASo7E1Sn4ECggEAVmEWbpi260VFaTCK
|
||||
gJCe4xPRCh1htkLQl4i0Xed4LkQYS33ZIWFvPrysosd8/fNKoFU/rLj3KWV1ei2Z
|
||||
3lFVZvW0manAo2X8r6FVs7xjW4BitRIbFEPKsAIr2JOt0aBmfDM4ySYyOvLSiE1z
|
||||
5cxWIC5B8u1m0MBDkSQ0Rj6etaxb73y0GX5tcmyGpD2X+ngxPr+cjss+5SohV/PG
|
||||
LqnwRcdTjtRFmvUcUWzgZfBgNEK0gIt37U3H/mgezJKZ4caBIM2zOvq+tA5q+Rbi
|
||||
wkcnIJUsfALRHYKGLNLH8CJwoXtidDZoFJiQrXvZMVuVNt8lm81GZNSvgrLGNVRF
|
||||
FPN1rQKCAQEApwToPn9gQhCNW/akfXGk+Si1el+/T5grevoiWgfE74Nylkkue1sg
|
||||
FaiuuYslBAo2xsHB2MRo64xjpbuOuC0mcO68lznhklzVqQbPKnlBas9CLUsg3m5A
|
||||
RtB9T63tjEVXoluJ/Rk7YvlZQQqpnSJQmW8/sV3112yYVypSx/A6CzlMK3v81QEU
|
||||
7JMuEcehLQJoRSXP6FhTyEAwt74yXxW+Iu2cUZAlqrro0i8chewaJGjQQ1V87Z9U
|
||||
YkEuKra0MUoAViBH5P6gdRNJcHXOniGheuqFOYMSSV1I0tB73GFO4m8ls0ljASOO
|
||||
0qNwGW2GD+8Nvo2dcCwFp70w3cdYl3/UAQKCAQEAsR+GKFH8i3spv1PHj5DibY/D
|
||||
hdhvYct/KDspHj2d5nT9DoB1reghW62Ty4wPx7JMioYJImA51NqJ/VErsnKOMJ54
|
||||
LQvmNO83QkqumC85T4nqBDIjiAK1lTqAbi/YJRU/4YoDgMc0SGdfYRrlNtm5/Mgy
|
||||
qaw0xprAST90rnSvrKVIO4Bc3HL3pRS0aQFpRJfbpB9xhd2FYB87A2dbSq4cPlDo
|
||||
xgzkvYrlgVTl1iwwTsIeGfEGZJ74T7nX2E03dfI83Xh5FsKUxN9Z2VEX/p3AaOF/
|
||||
9ipNhq4ounNz9S6uo5D+eOcoZ9gKJ9LXXknV2J5xEu2H1+ke8habRCBXmYjamQ==
|
||||
-----END RSA PRIVATE KEY-----
|
14
certs/san.cnf
Normal file
14
certs/san.cnf
Normal file
@ -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
|
10
certs/trust_ca.cmd
Normal file
10
certs/trust_ca.cmd
Normal file
@ -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
|
3
certs/trust_ca_macos.sh
Executable file
3
certs/trust_ca_macos.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db ca.crt
|
14
certs/trust_ca_ubuntu_firefox.sh
Executable file
14
certs/trust_ca_ubuntu_firefox.sh
Executable file
@ -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
|
191
complicated_sam_hc_auth.py
Normal file
191
complicated_sam_hc_auth.py
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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'<E>'):
|
||||
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'<E>'
|
||||
|
||||
def decrypt(key, data):
|
||||
# funciton `recvall` already ensures that data will end with b'<E>'
|
||||
assert data.endswith(b'<E>')
|
||||
|
||||
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<E>' % (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<E>', 8003, 'Failed to send test packet')
|
||||
ret = recvall(conn, 8005, 'Service check failed') == b'04<rc=2>OK<E>'
|
||||
|
||||
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<id=12>%s<rn=8>%s<E>' % (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<au=32>................................<E>'
|
||||
if not (data.startswith(b'02<au=32>') and data.endswith(b'<E>')):
|
||||
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<au=16>%s<se=20>%s<E>' % (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'04<rc=2>OK<si=256>' ...(256bytes) b'<E>'
|
||||
if not (data.startswith(b'04<rc=2>OK<si=256>') and data.endswith(b'<E>')):
|
||||
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)
|
121
cryptos.py
Normal file
121
cryptos.py
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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'))
|
BIN
docs/chrome-foxyproxy-01.png
Normal file
BIN
docs/chrome-foxyproxy-01.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
BIN
docs/chrome-foxyproxy-02.png
Normal file
BIN
docs/chrome-foxyproxy-02.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
docs/chrome-foxyproxy-03.png
Normal file
BIN
docs/chrome-foxyproxy-03.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
BIN
docs/chrome-foxyproxy-04.png
Normal file
BIN
docs/chrome-foxyproxy-04.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
BIN
docs/chrome-foxyproxy-05.png
Normal file
BIN
docs/chrome-foxyproxy-05.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
23
docs/setup-socks5-proxy-chrome-foxyproxy.md
Normal file
23
docs/setup-socks5-proxy-chrome-foxyproxy.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Chrome FoxyProxy 使用說明 / Chrome FoxyProxy Usage
|
||||
|
||||
## 新增 Proxy / Config new proxy
|
||||
|
||||
### 開啟 FoxyProxy 設定 / Open "options" of FoxyProxy
|
||||
|
||||

|
||||
|
||||
### 按下 "Add New Proxy" / Click "Add New Proxy"
|
||||
|
||||

|
||||
|
||||
### 填寫 Proxy 資料並且按下 "Save" / Fill proxy config and click "Save"
|
||||
|
||||

|
||||
|
||||
## 啟用 Proxy / Enable Proxy
|
||||
|
||||

|
||||
|
||||
## 停用 Proxy / Disable Proxy
|
||||
|
||||

|
21
errors.py
Normal file
21
errors.py
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
class ServiceError(Exception):
|
||||
def __init__(self, error_code, description, *args):
|
||||
super().__init__(*args)
|
||||
self.error_code = error_code
|
||||
self.description = description
|
170
hccard.py
Normal file
170
hccard.py
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#!/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())
|
144
install-packages.py
Normal file
144
install-packages.py
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
357
pysoxy.py
Normal file
357
pysoxy.py
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
# -*- 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()
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
cryptography
|
||||
hexdump
|
||||
pycryptodomex
|
||||
pyscard
|
||||
websockets
|
199
server.py
Normal file
199
server.py
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#!/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)
|
Reference in New Issue
Block a user