├── .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 | 
8 |
9 | ### 按下 "Add New Proxy" / Click "Add New Proxy"
10 |
11 | 
12 |
13 | ### 填寫 Proxy 資料並且按下 "Save" / Fill proxy config and click "Save"
14 |
15 | 
16 |
17 | ## 啟用 Proxy / Enable Proxy
18 |
19 | 
20 |
21 | ## 停用 Proxy / Disable Proxy
22 |
23 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------