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