[Python學習筆記-003] 使用PyOTP獲取基於OTOP算法的動態口令


建立安全的VPN連接,不僅需要輸入用戶名和密碼,還需要輸入動態口令(token)。作為一個懶人,我更喜歡什么手工輸入都不需要,既不需要輸入password,也不需要輸入token。也就是說,只需一個命令就能徑直連接上VPN,那自然是極好滴。那么,懶人的願望能實現嗎?答案是肯定的!本文將基於FreeOTP 支持的TOTP(Time-based One-Time Password)算法,介紹如何利用Python代碼自動獲取動態口令(token),進而利用Expect實現一個自動連接VPN的Bash腳本。

PyOTP是一套開源的函數庫,可用來計算基於TOTP算法的Token。有關TOTP算法的細節,本文不做介紹,如有興趣請參考這里

1. 下載PyOTP

huanli@ThinkPadT460:tmp$ git clone https://github.com/pyotp/pyotp.git
Cloning into 'pyotp'...
remote: Counting objects: 601, done.
remote: Total 601 (delta 0), reused 0 (delta 0), pack-reused 601
Receiving objects: 100% (601/601), 165.02 KiB | 207.00 KiB/s, done.
Resolving deltas: 100% (297/297), done.
huanli@ThinkPadT460:tmp$ 
huanli@ThinkPadT460:tmp$ tree /tmp/pyotp/src
/tmp/pyotp/src
└── pyotp
    ├── compat.py
    ├── hotp.py
    ├── __init__.py
    ├── otp.py
    ├── totp.py
    └── utils.py

1 directory, 6 files
huanli@ThinkPadT460:tmp$ 

2. 使用PyOTP

huanli@ThinkPadT460:tmp$ export PYTHONPATH=/tmp/pyotp/src:$PYTHONPATH
huanli@ThinkPadT460:tmp$ python
...<snip>...
>>> import base64
>>> import pyotp
>>> s = 'Hello World'
>>> secret = base64.b32encode(s)
>>> totp = pyotp.TOTP(secret)
>>> token = totp.now()
>>> print token
338462
>>>

由此可見,通過pyotp.TOTP()獲取token非常容易。我們將調用到的核心代碼實現如下:

# https://github.com/pyotp/pyotp/blob/master/src/pyotp/otp.py
    ..
    10  class TOTP(OTP):
    11      """
    12      Handler for time-based OTP counters.
    13      """
    14      def __init__(self, *args, **kwargs):
    15          """
    16          :param interval: the time interval in seconds
    17              for OTP. This defaults to 30.
    18          :type interval: int
    19          """
    20          self.interval = kwargs.pop('interval', 30)
    21          super(TOTP, self).__init__(*args, **kwargs)
    ..
    37      def now(self):
    38          """
    39          Generate the current time OTP
    40  
    41          :returns: OTP value
    42          :rtype: str
    43          """
    44          return self.generate_otp(self.timecode(datetime.datetime.now()))
    ..

# https://github.com/pyotp/pyotp/blob/master/src/pyotp/otp.py
    ..
     8  class OTP(object):
     9      """
    10      Base class for OTP handlers.
    11      """
    12      def __init__(self, s, digits=6, digest=hashlib.sha1):
    13          """
    14          :param s: secret in base32 format
    15          :type s: str
    16          :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
    17          :type digits: int
    18          :param digest: digest function to use in the HMAC (expected to be sha1)
    19          :type digest: callable
    20          """
    21          self.digits = digits
    22          self.digest = digest
    23          self.secret = s
    24
    25      def generate_otp(self, input):
    26          """
    27          :param input: the HMAC counter value to use as the OTP input.
    28              Usually either the counter, or the computed integer based on the Unix timestamp
    29          :type input: int
    30          """
    31          if input < 0:
    32              raise ValueError('input must be positive integer')
    33          hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest)
    34          hmac_hash = bytearray(hasher.digest())
    35          offset = hmac_hash[-1] & 0xf
    36          code = ((hmac_hash[offset] & 0x7f) << 24 |
    37                  (hmac_hash[offset + 1] & 0xff) << 16 |
    38                  (hmac_hash[offset + 2] & 0xff) << 8 |
    39                  (hmac_hash[offset + 3] & 0xff))
    40          str_code = str(code % 10 ** self.digits)
    41          while len(str_code) < self.digits:
    42              str_code = '0' + str_code
    43  
    44          return str_code
    ..

下面給出完整的Python腳本:

  • vpn_token.py
 1 #!/usr/bin/python
 2 import sys
 3 import datetime
 4 import time
 5 
 6 def main(argc, argv):
 7     if argc != 3:
 8         sys.stderr.write("Usage: %s <token secret> <pyotp path>\n" % argv[0])
 9         return 1
10 
11     token_secret = argv[1]
12     pyotp_path = argv[2]
13 
14     sys.path.append(pyotp_path)
15     import pyotp
16     totp = pyotp.TOTP(token_secret)
17 
18     #
19     # The token is expected to be valid in 5 seconds,
20     # else sleep 5s and retry
21     #
22     while True:
23         tw = datetime.datetime.now() + datetime.timedelta(seconds=5)
24         token = totp.now()
25         if totp.verify(token, tw):
26             print "%s" % token
27             return 0
28         time.sleep(5)
29 
30     return 1
31 
32 if __name__ == '__main__':
33     sys.exit(main(len(sys.argv), sys.argv))
  • 來自Terminal的Token: 707907

  • 來自手機的Token: 707907

由此可見,跟PyOTP計算出的Token碼完全一致。於是,我們就可以利用Expect實現完全自動的VPN連接。例如: (注: 這里使用sexpect

  • autovpn.sh (完整的代碼請戳這里
  1 #!/bin/bash
  2 
  3 function get_vpn_user
  4 {
  5         typeset user=${VPN_USER:-"$(id -un)"}
  6         echo "$user"
  7         return 0
  8 }
  9 
 10 function get_vpn_password
 11 {
 12         typeset password=${VPN_PASSWORD:-"$(eval $($VPN_PASSWORD_HOOK))"}
 13         echo "$password"
 14         return 0
 15 }
 16 
 17 function get_vpn_token
 18 {
 19         typeset f_py_cb=/tmp/.vpn_token.py
 20         cat > $f_py_cb << EOF
 21 #!/usr/bin/python
 22 import sys
 23 import datetime
 24 import time
 25 
 26 def main(argc, argv):
 27     if argc != 3:
 28         sys.stderr.write("Usage: %s <token secret> <pyotp path>\\n" % argv[0])
 29         return 1
 30 
 31     token_secret = argv[1]
 32     pyotp_path = argv[2]
 33 
 34     sys.path.append(pyotp_path)
 35     import pyotp
 36     totp = pyotp.TOTP(token_secret)
 37 
 38     #
 39     # The token is expected to be valid in 5 seconds,
 40     # else sleep 5s and retry
 41     #
 42     while True:
 43         tw = datetime.datetime.now() + datetime.timedelta(seconds=5)
 44         token = totp.now()
 45         if totp.verify(token, tw):
 46             print "%s" % token
 47             return 0
 48         time.sleep(5)
 49 
 50     return 1
 51 
 52 if __name__ == '__main__':
 53     argv = sys.argv
 54     argc = len(argv)
 55     sys.exit(main(argc, argv))
 56 EOF
 57 
 58         typeset pyotp_path=$VPN_PYOTP_PATH
 59         typeset token_secret=$VPN_TOKEN_SECRET
 60         if [[ -z $token_secret ]]; then
 61                 token_secret=$(eval $($VPN_TOKEN_SECRET_HOOK))
 62         fi
 63 
 64         python $f_py_cb $token_secret $pyotp_path
 65         typeset ret=$?
 66         rm -f $f_py_cb
 67         return $ret
 68 }
 69 
 70 function get_vpn_conf
 71 {
 72         typeset conf=$VPN_CONF
 73         echo "$conf"
 74         return 0
 75 }
 76 
 77 function check_sexpect
 78 {
 79         type sexpect 2>&1 | egrep 'not found' > /dev/null 2>&1
 80         (( $? != 0 )) && return 0
 81         return 1
 82 }
 83 
 84 vpn_user=$(get_vpn_user)
 85 (( $? != 0 )) && exit 1
 86 vpn_password=$(get_vpn_password)
 87 (( $? != 0 )) && exit 1
 88 vpn_token=$(get_vpn_token)
 89 (( $? != 0 )) && exit 1
 90 vpn_conf=$(get_vpn_conf)
 91 (( $? != 0 )) && exit 1
 92 
 93 check_sexpect || exit 1
 94 
 95 export SEXPECT_SOCKFILE=/tmp/sexpect-ssh-$$.sock
 96 trap '{ sexpect close && sexpect wait; } > /dev/null 2>&1' EXIT
 97 
 98 sexpect spawn sudo openvpn --config $vpn_conf
 99 sexpect set -timeout 60 # XXX: 'set' should be invoked after server is running
100 
101 while :; do
102         sexpect expect -nocase -re "Username:|Password:"
103         ret=$?
104         if (( $ret == 0 )); then
105                 out=$(sexpect expect_out)
106                 if [[ $out == *"Username:"* ]]; then
107                         sexpect send -enter "$vpn_user"
108                 elif [[ $out == *"Password:"* ]]; then
109                         sexpect send -enter "$vpn_password$vpn_token"
110                         break
111                 else
112                         echo "*** unknown catch: $out" >&2
113                         exit 1
114                 fi
115         elif sexpect chkerr -errno $ret -is eof; then
116                 sexpect wait
117                 exit 0
118         elif sexpect chkerr -errno $ret -is timeout; then
119                 sexpect close
120                 sexpect wait
121                 echo "*** timeout waiting for username/password prompt" >&2
122                 exit 1
123         else
124                 echo "*** unknown error: $ret" >&2
125                 exit 1
126         fi
127 done
128 
129 sexpect interact
  • 運行autovpn.sh
huanli@ThinkPadT460:~$ ./autovpn.sh 
Sat Aug 11 22:32:17 2018 OpenVPN 2.4.6 x86_64-redhat-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Apr 26 2018
Sat Aug 11 22:32:17 2018 library versions: OpenSSL 1.1.0h-fips  27 Mar 2018, LZO 2.08
Enter Auth Username: huanli
Enter Auth Password: ****************
Sat Aug 11 22:32:17 2018 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
...<snip>...
Sat Aug 11 22:32:20 2018 GID set to openvpn
Sat Aug 11 22:32:20 2018 UID set to openvpn
Sat Aug 11 22:32:20 2018 Initialization Sequence Completed

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM