ezpass

2023/JUL/15

更新履歴
日付 変更内容
2023/JUL/09 新規作成
バージョン 2 追加
2023/JUL/15 バージョン 3 追加

目次


本業が忙し過ぎて kon_page の更新が途絶えて久しく。

ですが、ふと思い出したように、ツールを作ってみました。


パスワードの入力補助ツール (バージョン 1)

端末を操作しているとパスワードの入力を多々求められます。

公開鍵をちゃんと登録すればそれで問題無いのかもですが、、、

ちょっとしたツールを自作して試してみました。

動作原理

python の pty パッケージの spawn() を使って、端末の画面出力とキー入力を見張ります。

" passwd: " という文字列に反応して、そのときの画面の行と、キー入力した文字を登録します。

次回から同じ画面の行が現れて、1.5秒キー入力が無いと、登録されている文字列をキー入力します。

登録された内容はホームディレクトリに .ezpass というファイルに記録されます。

キー入力は、python の Xlib パッケージを使って、Xのキーイベントを発行するようにしています。

せっかく pty 使っているので、pty でスマートに何とかしたかったのですが、 いまいちよく解らず、Xserverに頼ってしまいました。

注意

便利ですがセキュリティのへったくりもないので超危険です。

自己責任で使ってください。

動作環境

とりあえず Ubuntu 18.04 でしか試せていません。

kon_ut を適当に使っています。

kon_pageのpythonモジュールのインストール などご参考に。

キー入力でXlibを使っているので、python-xlib をインストールして使っています。

$ sudo apt install python-xlib

ソースコード

本体

ezpass.py
#!/usr/bin/env python3

import sys
import os
import pty
import threading

import empty
import thr
import cmd_ut
import yaml_ut
import dbg

sec = 1.5

def b_to_s( b ):
	try:
		s = b.decode()
	except:
		return None
	return s

def key_ev( s ):
	DIR = os.path.dirname( __file__ )
	cmd = 'echo {} | {}/key_ev.py'.format( s, DIR )
	cmd_ut.call( cmd )

def dic_new():
	d = {}
	path = os.path.expandvars( "$HOME/.ezpass" )
	if os.path.exists( path ):
		d = yaml_ut.load_fn( path, {} )

	def save():
		yaml_ut.save( d, path )

	def get( k ):
		return d.get( k )

	def put( k, v ):
		if k in d:
			return
		d[ k ] = v
		save()

	return empty.new( locals() )

dic = dic_new()

def buf_new():
	e = empty.new()
	e.bf = ''
	e.tmrs = []

	def put( s ):
		if s.endswith( '\n' ):
			e.bf = ''
			return

		e.bf += s

		s = dic.get( e.bf )
		if s:
			tmr = threading.Timer( sec, key_ev, ( s, ) )
			tmr.start()
			e.tmrs.append( tmr )

	def key_in( s ):
		if e.bf.endswith( ' password: ' ):
			dic.put( e.bf, s )

	def tmr_cancel():
		while e.tmrs:
			tmr = e.tmrs.pop( 0 )
			tmr.cancel()

	def read( fd ):
		b = os.read( fd, 1024 )
		s = b_to_s( b )
		if s:
			put( s )
		return b

	return empty.add( e, locals() )

buf = buf_new()

def ibuf_new():
	e = empty.new()
	e.bf = ''

	def put( s ):
		if s == '\r':
			buf.key_in( e.bf )
			e.bf = ''
			return

		e.bf += s

	def read( fd ):
		b = os.read( fd, 1024 )
		buf.tmr_cancel()
		s = b_to_s( b )
		if s:
			put( s )
		return b

	return empty.add( e, locals() )

ibuf = ibuf_new()

argv = sys.argv[ 1 : ]

if not argv:
	argv = [ 'bash' ]

pty.spawn( argv, buf.read, ibuf.read )

# EOF

Xlibキー入力用

key_ev.py
#!/usr/bin/env python

import sys
import time

from Xlib import X, display, XK

import dbg

def run():
	dpy = display.Display()

	def c_to_string_lst( c ):
		dic = {
			' ': 'space',
			'\n': 'Return',
			'\t': 'Tab',
			'\b': 'BackSpace',
			'_': 'underscore',
		}
		if c in dic:
			c = dic.get( c )
		lst = [ c ]
		if c.isupper() or c == 'underscore':
			lst = [ 'Shift_L' ] + lst
		return lst

	def string_to_kcode( s ):
		ksym = XK.string_to_keysym( s )
		kcode = dpy.keysym_to_keycode( ksym )
		return kcode

	def fake_kcode( ev, kcode, wait_sec ):
		dpy.xtest_fake_input( ev, detail=kcode )
		dpy.flush()
		time.sleep( wait_sec )

	def press_release_kcodes( kcodes, wait_sec ):
		for kcode in kcodes:
			fake_kcode( X.KeyPress, kcode, wait_sec )
		for kcode in kcodes[ :: -1 ]:
			fake_kcode( X.KeyRelease, kcode, wait_sec )

	wait_sec = 0.01
	s = sys.stdin.read()
	for c in s:
		lst = c_to_string_lst( c )
		kcodes = list( map( string_to_kcode, lst ) )
	        press_release_kcodes( kcodes, wait_sec )

if __name__ == "__main__":
	run()
# EOF

使用方法

端末を開いて

ezpass.py を実行すると内部で bash が実行されます。

あとは、ezpass.py が画面とキー入力を見張ります。

トリガ

" password: " の文字列出力をトリガに動作します。

1.5 秒待つと、登録されているパスワードをキー入力します。

1.5 秒以内に手動で何かキー入力すると、自動入力はキャンセルされます。

コマンドライン引数

ezpass.py のコマンドライン引数として、bashの変わりに実行したいコマンドを指定可能です。

個人的にはいつもscreenコマンドで複数の画面を切り替えて作業しているので、

$ ./ezpass.py screen -x

などと使ってみたいですね。

(まだ試せていません...)

登録ファイル

~/.ezpass として YAML 形式で記録されます。

お試しの例

$ ./ezpass.py

# 内部で bash が実行されます

kondoh@autoware-ThinkPad:~/kon_page/ezpass$

kondoh@autoware-ThinkPad:~/kon_page/ezpass$ ssh localhost
kondoh@localhost's password: # ここでパスワードを入力

# まずは普通にログインします

Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 5.4.0-148-generic x86_64)
  :

# 一旦、ログアウトします。

kondoh@autoware-ThinkPad:~$ exit
ログアウト
Connection to localhost closed.
kondoh@autoware-ThinkPad:~/kon_page/ezpass$


# そしてもう一度

kondoh@autoware-ThinkPad:~/kon_page/ezpass$ ssh localhost
kondoh@localhost's password: # ここで1.5秒待つと

# ezpass.py が先程入力したパスワードを代わりに入力してくれてログイン出来ます

Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 5.4.0-148-generic x86_64)
  :

# ログアウトします。

kondoh@autoware-ThinkPad:~/kon_page/ezpass$ exit
ログアウト
Connection to localhost closed.

kondoh@autoware-ThinkPad:~/kon_page/ezpass$ exit

# さらに exit すると、内部で起動している bash が終了し、ezpass.py が終了します。

$

# ホームディレクトリに .ezpass が生成されてパスワードが記録されています。

$ cat ~/.ezpass
'kondoh@localhost''s password: ': xxxxx

# 取扱注意です。くれぐれも自己責任で試してください。


パスワードの入力補助ツール (バージョン 2)

Xlibのキーイベントの入力をやめてptyでなんとかしてみました。

その代わり、1.5秒待って、入力なければ勝手に入力してくれるというv1の動作は無理で、 パスワードのキー入力のきっかけが必要で、仕様を変更してます。

パスワードの入力が必要な場面で、タブキーの入力で登録しているパスワードが入力されます。

動作環境

まだまだ Ubuntu 18.04 でしか試せていません。

v1 と同じく kon_ut を適当に使っています。

kon_pageのpythonモジュールのインストール などご参考に。

キー入力でXlibを使っているので、python-xlib をインストールして使っています。

python-xlib は不要です。

ソースコード

v2/ezpass.py
#!/usr/bin/env python3

import sys
import os
import pty

import empty
import yaml_ut
import dbg

sec = 1.5

def b_to_s( b ):
	try:
		s = b.decode()
	except:
		return None
	return s

def dic_new():
	d = {}
	path = os.path.expandvars( "$HOME/.ezpass" )
	if os.path.exists( path ):
		d = yaml_ut.load_fn( path, {} )

	def save():
		yaml_ut.save( d, path )

	def get( k ):
		return d.get( k )

	def put( k, v ):
		if k in d:
			return
		d[ k ] = v
		save()

	return empty.new( locals() )

dic = dic_new()

def buf_new():
	e = empty.new()
	e.bf = ''
	e.tmrs = []

	def put( s ):
		if s.endswith( '\n' ):
			e.bf = ''
			return

		e.bf += s

	def key_in( s ):
		if e.bf.endswith( ' password: ' ):
			dic.put( e.bf, s )

	def get():
		return e.bf

	def read( fd ):
		b = os.read( fd, 1024 )
		s = b_to_s( b )
		if s:
			put( s )
		return b

	return empty.add( e, locals() )

buf = buf_new()

def ibuf_new():
	e = empty.new()
	e.bf = ''

	def put( s ):
		if s == '\r':
			buf.key_in( e.bf )
			e.bf = ''
			return

		e.bf += s

		if e.bf == '\t':
			return dic.get( buf.get() )

		return None

	def read( fd ):
		b = os.read( fd, 1024 )
		s = b_to_s( b )
		if s:
			s = put( s )
			if s:
				s += '\r'
				b = s.encode()
		return b

	return empty.add( e, locals() )

ibuf = ibuf_new()

argv = sys.argv[ 1 : ]

if not argv:
	argv = [ 'bash' ]

pty.spawn( argv, buf.read, ibuf.read )

# EOF

コマンドライン引数

v1 と同じです。

ezpass.py のコマンドライン引数として、bashの変わりに実行したいコマンドを指定可能です。

個人的にはいつもscreenコマンドで複数の画面を切り替えて作業しているので、

$ ./ezpass.py screen -x

などと使ってみたいですね。

(まだ試せていません...)

登録ファイル

v1 と同じです。

~/.ezpass として YAML 形式で記録されます。

使用方法

端末を開いて

ezpass.py を実行すると内部で bash が実行されます。

あとは、ezpass.py が画面とキー入力を見張ります。

$ ./ezpass.py

# 内部で bash が実行されます

kondoh@autoware-ThinkPad:~/kon_page/ezpass$

kondoh@autoware-ThinkPad:~/kon_page/ezpass$ ssh localhost
kondoh@localhost's password: # ここでパスワードを入力

# まずは普通にログインします

Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 5.4.0-148-generic x86_64)
  :

# 一旦、ログアウトします。

kondoh@autoware-ThinkPad:~$ exit
ログアウト
Connection to localhost closed.
kondoh@autoware-ThinkPad:~/kon_page/ezpass$


# そしてもう一度

kondoh@autoware-ThinkPad:~/kon_page/ezpass$ ssh localhost
kondoh@localhost's password: # ここでタブキーを入力すると

# ezpass.py が先程入力したパスワードを代わりに入力してくれてログイン出来ます

Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 5.4.0-148-generic x86_64)
  :

# ログアウトします。

kondoh@autoware-ThinkPad:~/kon_page/ezpass$ exit
ログアウト
Connection to localhost closed.

kondoh@autoware-ThinkPad:~/kon_page/ezpass$ exit

# さらに exit すると、内部で起動している bash が終了し、ezpass.py が終了します。

$

# ホームディレクトリに .ezpass が生成されてパスワードが記録されています。

$ cat ~/.ezpass
'kondoh@localhost''s password: ': xxxxx

# 取扱注意です。くれぐれも自己責任で試してください。


パスワードの入力補助ツール (バージョン 3)

実行すると、端末のサイズが変わってしまう事に気づきました。

いつも使っているエディタ emacs -nw を実行すると、 端末の中に小さい範囲い emacs の世界が ...

適当に ioctl で端末のサイズを取得して、 初回の pty fd の操作時に、そのサイズを設定するようにしてみました。

あと、手持ちの古いMac bookで試してみると、パスワードを促すメッセージが、 "Password:" だったりして反応せず。

def key_in( s ):
	if e.bf.endswith( ' password: ' ):
		dic.put( e.bf, s )

とソースに埋め込んで固定だったので、 起動時のコマンドライン引数 -pw_msg="xxxx" で、この文字列を追加できるようにしてみました。

ソースコード

v3/ezpass.py
#!/usr/bin/env python3

import sys
import os
import pty
import termios
import struct
import fcntl

import empty
import yaml_ut
import dbg

sec = 1.5

argv = sys.argv[ 1 : ]

pw_msgs = [ ' password: ' ]

k = '-pw_msg='
for i in range( len( argv ) ):
	if argv[ i ].startswith( k ):
		s = argv.pop( i )
		pw_msgs.append( s[ len( k ) : ] )

def b_to_s( b ):
	try:
		s = b.decode()
	except:
		return None
	return s

def dic_new():
	d = {}
	path = os.path.expandvars( "$HOME/.ezpass" )
	if os.path.exists( path ):
		d = yaml_ut.load_fn( path, {} )

	def save():
		yaml_ut.save( d, path )

	def get( k ):
		return d.get( k )

	def put( k, v ):
		if k in d:
			return
		d[ k ] = v
		save()

	return empty.new( locals() )

dic = dic_new()

def get_wsz( fd ):
	pfmt = 'HHHH'
	wsz = struct.pack( pfmt, 0, 0, 0, 0 )
	wsz = fcntl.ioctl( fd, termios.TIOCGWINSZ, wsz )
	( rows, cols, xpix, ypix ) = struct.unpack( pfmt, wsz )
	return ( cols, rows )

def set_wsz( fd, cols, rows ):
	pfmt = 'HHHH'
	wsz = struct.pack( pfmt, rows, cols, 0, 0 )
	fcntl.ioctl( fd, termios.TIOCSWINSZ, wsz )

def buf_new():
	e = empty.new()
	e.bf = ''
	e.wsz=None

	def put( s ):
		if s.endswith( '\n' ):
			e.bf = ''
			return

		e.bf += s

	def key_in( s ):
		for pw_msg in pw_msgs:
			if e.bf.endswith( pw_msg ):
				dic.put( e.bf, s )
				break

	def get():
		return e.bf

	def read( fd ):
		if e.wsz is None:
			set_sz( fd )

		b = os.read( fd, 1024 )
		s = b_to_s( b )
		if s:
			put( s )
		return b

	def set_sz( fd ):
		( cols, rows ) = e.wsz = get_wsz( sys.stdout.fileno() )
		set_wsz( fd, cols, rows )

	return empty.add( e, locals() )

buf = buf_new()

def ibuf_new():
	e = empty.new()
	e.bf = ''

	def put( s ):
		if s == '\r':
			buf.key_in( e.bf )
			e.bf = ''
			return

		e.bf += s

		if e.bf == '\t':
			return dic.get( buf.get() )

		return None

	def read( fd ):
		b = os.read( fd, 1024 )
		s = b_to_s( b )
		if s:
			s = put( s )
			if s:
				s += '\r'
				b = s.encode()
		return b

	return empty.add( e, locals() )

ibuf = ibuf_new()

if not argv:
	argv = [ 'bash' ]

pty.spawn( argv, buf.read, ibuf.read )

# EOF

v3.patch
diff -ur v2/ezpass.py v3/ezpass.py
--- v2/ezpass.py	2023-07-09 12:27:46.601592416 +0900
+++ v3/ezpass.py	2023-07-15 13:38:54.431241842 +0900
@@ -3,6 +3,9 @@
 import sys
 import os
 import pty
+import termios
+import struct
+import fcntl

 import empty
 import yaml_ut
@@ -10,6 +13,16 @@

 sec = 1.5

+argv = sys.argv[ 1 : ]
+
+pw_msgs = [ ' password: ' ]
+
+k = '-pw_msg='
+for i in range( len( argv ) ):
+	if argv[ i ].startswith( k ):
+		s = argv.pop( i )
+		pw_msgs.append( s[ len( k ) : ] )
+
 def b_to_s( b ):
 	try:
 		s = b.decode()
@@ -39,10 +52,22 @@

 dic = dic_new()

+def get_wsz( fd ):
+	pfmt = 'HHHH'
+	wsz = struct.pack( pfmt, 0, 0, 0, 0 )
+	wsz = fcntl.ioctl( fd, termios.TIOCGWINSZ, wsz )
+	( rows, cols, xpix, ypix ) = struct.unpack( pfmt, wsz )
+	return ( cols, rows )
+
+def set_wsz( fd, cols, rows ):
+	pfmt = 'HHHH'
+	wsz = struct.pack( pfmt, rows, cols, 0, 0 )
+	fcntl.ioctl( fd, termios.TIOCSWINSZ, wsz )
+
 def buf_new():
 	e = empty.new()
 	e.bf = ''
-	e.tmrs = []
+	e.wsz=None

 	def put( s ):
 		if s.endswith( '\n' ):
@@ -52,19 +77,28 @@
 		e.bf += s

 	def key_in( s ):
-		if e.bf.endswith( ' password: ' ):
-			dic.put( e.bf, s )
+		for pw_msg in pw_msgs:
+			if e.bf.endswith( pw_msg ):
+				dic.put( e.bf, s )
+				break

 	def get():
 		return e.bf

 	def read( fd ):
+		if e.wsz is None:
+			set_sz( fd )
+
 		b = os.read( fd, 1024 )
 		s = b_to_s( b )
 		if s:
 			put( s )
 		return b

+	def set_sz( fd ):
+		( cols, rows ) = e.wsz = get_wsz( sys.stdout.fileno() )
+		set_wsz( fd, cols, rows )
+
 	return empty.add( e, locals() )

 buf = buf_new()
@@ -100,8 +134,6 @@

 ibuf = ibuf_new()

-argv = sys.argv[ 1 : ]
-
 if not argv:
 	argv = [ 'bash' ]

使用方法

パスワード入力トリガとして、例えば "Password:" と "passwd: " を追加して動作させる場合は、

$ ./ezpass.py -pw_msg="Password:" -pw_msg="passwd: "
$ ./ezpass.py -pw_msg="Password:" -pw_msg="passwd: " screen -x

などと指定して起動します。