[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: , done.
remote: Total (delta ), reused (delta ), pack-reused
Receiving objects: % (/), 165.02 KiB | 207.00 KiB/s, done.
Resolving deltas: % (/), 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 directory, 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 = '' + str_code
43
44 return str_code
..

下面给出完整的Python脚本:

  • vpn_token.py
 #!/usr/bin/python
import sys
import datetime
import time def main(argc, argv):
if argc != 3:
sys.stderr.write("Usage: %s <token secret> <pyotp path>\n" % argv[0])
return 1 token_secret = argv[1]
pyotp_path = argv[2] sys.path.append(pyotp_path)
import pyotp
totp = pyotp.TOTP(token_secret) #
# The token is expected to be valid in 5 seconds,
# else sleep 5s and retry
#
while True:
tw = datetime.datetime.now() + datetime.timedelta(seconds=5)
token = totp.now()
if totp.verify(token, tw):
print "%s" % token
return 0
time.sleep(5) return 1 if __name__ == '__main__':
sys.exit(main(len(sys.argv), sys.argv))
  • 来自Terminal的Token:

[Python学习笔记-003] 使用PyOTP获取基于OTOP算法的动态口令

  • 来自手机的Token:

[Python学习笔记-003] 使用PyOTP获取基于OTOP算法的动态口令

由此可见,跟PyOTP计算出的Token码完全一致。于是,我们就可以利用Expect实现完全自动的VPN连接。例如: (注: 这里使用sexpect

  • autovpn.sh (完整的代码请戳这里
 #!/bin/bash

 function get_vpn_user
{
typeset user=${VPN_USER:-"$(id -un)"}
echo "$user"
return
} function get_vpn_password
{
typeset password=${VPN_PASSWORD:-"$(eval $($VPN_PASSWORD_HOOK))"}
echo "$password"
return
} function get_vpn_token
{
typeset f_py_cb=/tmp/.vpn_token.py
cat > $f_py_cb << EOF
#!/usr/bin/python
import sys
import datetime
import time def main(argc, argv):
if argc != :
sys.stderr.write("Usage: %s <token secret> <pyotp path>\\n" % argv[])
return token_secret = argv[]
pyotp_path = argv[] sys.path.append(pyotp_path)
import pyotp
totp = pyotp.TOTP(token_secret) #
# The token is expected to be valid in seconds,
# else sleep 5s and retry
#
while True:
tw = datetime.datetime.now() + datetime.timedelta(seconds=)
token = totp.now()
if totp.verify(token, tw):
print "%s" % token
return
time.sleep() return if __name__ == '__main__':
argv = sys.argv
argc = len(argv)
sys.exit(main(argc, argv))
EOF typeset pyotp_path=$VPN_PYOTP_PATH
typeset token_secret=$VPN_TOKEN_SECRET
if [[ -z $token_secret ]]; then
token_secret=$(eval $($VPN_TOKEN_SECRET_HOOK))
fi python $f_py_cb $token_secret $pyotp_path
typeset ret=$?
rm -f $f_py_cb
return $ret
} function get_vpn_conf
{
typeset conf=$VPN_CONF
echo "$conf"
return
} function check_sexpect
{
type sexpect >& | egrep 'not found' > /dev/null >&
(( $? != )) && return
return
} vpn_user=$(get_vpn_user)
(( $? != )) && exit
vpn_password=$(get_vpn_password)
(( $? != )) && exit
vpn_token=$(get_vpn_token)
(( $? != )) && exit
vpn_conf=$(get_vpn_conf)
(( $? != )) && exit check_sexpect || exit export SEXPECT_SOCKFILE=/tmp/sexpect-ssh-$$.sock
trap '{ sexpect close && sexpect wait; } > /dev/null 2>&1' EXIT sexpect spawn sudo openvpn --config $vpn_conf
sexpect set -timeout # XXX: 'set' should be invoked after server is running while :; do
sexpect expect -nocase -re "Username:|Password:"
ret=$?
if (( $ret == )); then
out=$(sexpect expect_out)
if [[ $out == *"Username:"* ]]; then
sexpect send -enter "$vpn_user"
elif [[ $out == *"Password:"* ]]; then
sexpect send -enter "$vpn_password$vpn_token"
break
else
echo "*** unknown catch: $out" >&
exit
fi
elif sexpect chkerr -errno $ret -is eof; then
sexpect wait
exit
elif sexpect chkerr -errno $ret -is timeout; then
sexpect close
sexpect wait
echo "*** timeout waiting for username/password prompt" >&
exit
else
echo "*** unknown error: $ret" >&
exit
fi
done sexpect interact
  • 运行autovpn.sh
huanli@ThinkPadT460:~$ ./autovpn.sh
Sat Aug :: OpenVPN 2.4. x86_64-redhat-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Apr
Sat Aug :: library versions: OpenSSL 1.1.0h-fips Mar , LZO 2.08
Enter Auth Username: huanli
Enter Auth Password: ****************
Sat Aug :: NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
...<snip>...
Sat Aug :: GID set to openvpn
Sat Aug :: UID set to openvpn
Sat Aug :: Initialization Sequence Completed
上一篇:转:java 重定向和转发的区别


下一篇:YS动态口令系统接入流程