Skip to content

Commit 25fa30f

Browse files
committed
Prompt for 2FA code on initial login. Closes #14
1 parent cce402c commit 25fa30f

File tree

6 files changed

+61
-34
lines changed

6 files changed

+61
-34
lines changed

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ LLC.
1010

1111
- Auto-type username and/or password on selection. No clipboard copy/paste
1212
involved.
13+
- Login with 2FA code from Authenticator(TOTP), Email, or Yubikey (only these
14+
are supported by the Bitwarden CLI).
1315
- Select any single field and have it typed into the active window. Notes fields
1416
can be viewed line-by-line from within dmenu and the selected line will be
1517
typed when selected.
@@ -67,7 +69,8 @@ LLC.
6769
+ Available in the [Archlinux AUR][aur].
6870

6971
- If you start bwm for the first time without a config file, it will prompt
70-
you for server name and login email and save them in the config file.
72+
you for server name, login email, and 2FA type and save them in the config
73+
file.
7174

7275
- Copy config.ini.example to ~/.config/bwm/config.ini, or use it as a
7376
reference for additional options.
@@ -107,8 +110,8 @@ convenience for testing.
107110
## Usage
108111

109112
- Run script or bind to keystroke combination
110-
- Enter server URL (default `vault.bitwarden.com`) and login email if not
111-
entered into config.ini already.
113+
- Enter server URL (default `vault.bitwarden.com`), login email and 2FA type if
114+
not entered into config.ini already.
112115
- Start typing to match entries.
113116
- Hit Enter immediately after dmenu opens ("`View/Type individual entries`") to
114117
switch modes to view and/or type the individual fields for the entry. If

bwm.1

+22-18
Original file line numberDiff line numberDiff line change
@@ -10,46 +10,49 @@ vaults.
1010
\fB1.\fR Auto\-type username and/or password on selection. No clipboard
1111
copy/paste involved.
1212

13-
\fB2.\fR Use a custom Keepass 2.x style auto\-type sequence if you have one
13+
\fB2.\fR Login with 2FA code from Authenticator(TOTP), Email, or Yubikey (only
14+
these are supported by the Bitwarden CLI).
15+
16+
\fB3.\fR Use a custom Keepass 2.x style auto\-type sequence if you have one
1417
defined (except for character repetition and the \(aqspecial commands\(aq). Set
1518
it per entry and/or set a default in the config file for all entries.
1619

17-
\fB3.\fR Select any single field and have it typed into the active window. Notes
20+
\fB4.\fR Select any single field and have it typed into the active window. Notes
1821
fields can be viewed line\-by\-line from within dmenu and the selected line will
1922
be typed when selected.
2023

21-
\fB4.\fR Open the URL in the default web browser from the View/Type menu.
24+
\fB5.\fR Open the URL in the default web browser from the View/Type menu.
2225

23-
\fB5.\fR Alternate keyboard languages and layouts supported via xdotool or
26+
\fB6.\fR Alternate keyboard languages and layouts supported via xdotool or
2427
ydotool (for Wayland).
2528

26-
\fB6.\fR Edit entry title, username, URL and password (manually typed or
29+
\fB7.\fR Edit entry title, username, URL and password (manually typed or
2730
auto\-generate).
2831

29-
\fB7.\fR Edit notes using terminal or gui editor (set in config.ini, or uses
32+
\fB8.\fR Edit notes using terminal or gui editor (set in config.ini, or uses
3033
$EDITOR).
3134

32-
\fB8.\fR Add and Delete entries.
35+
\fB9.\fR Add and Delete entries.
3336

34-
\fB9.\fR Rename, move, delete and add folders and collections.
37+
\fB10.\fR Rename, move, delete and add folders and collections.
3538

36-
\fB10.\fR Move any item to or from an organization, including support for multiple collections.
39+
\fB11.\fR Move any item to or from an organization, including support for multiple collections.
3740

38-
\fB11.\fR Prompts for and saves initial server URL and login email.
41+
\fB12.\fR Prompts for and saves initial server URL and login email.
3942

40-
\fB12.\fR Define multiple vault URLs in the config file.
43+
\fB13.\fR Define multiple vault URLs in the config file.
4144

42-
\fB13.\fR Hide selected folders from the default and \(aqView/Type Individual
45+
\fB14.\fR Hide selected folders from the default and \(aqView/Type Individual
4346
entries\(aq views.
4447

45-
\fB14.\fR Configure session timeout.
48+
\fB15.\fR Configure session timeout.
4649

47-
\fB15. \fR Configure the characters and groups of characters used during
50+
\fB16. \fR Configure the characters and groups of characters used during
4851
password generation in the config file (see config.ini.example for
4952
instructions). Multiple character sets can be selected on the fly when using
5053
Rofi if the `-multi-select` option is passed via `dmenu_command`.
5154

52-
\fB16.\fR Optional Pinentry support for secure passphrase entry.
55+
\fB17.\fR Optional Pinentry support for secure passphrase entry.
5356

5457
.SH LICENSE
5558
Copyright © 2021 Scott Hansen <firecat4153@gmail.com>. Bitwarden-menu is
@@ -98,7 +101,8 @@ OR
98101

99102
.SH CONFIGURATION
100103
\fB1.\fR If you start bwm for the first time without a config file, it will
101-
prompt you for server name and login email and save them in the config file.
104+
prompt you for server name, login email and 2FA type and save them in the config
105+
file.
102106

103107
\fB2.\fR Copy config.ini.example to ~/.config/bwm/config.ini, or use it as a
104108
reference for additional options.
@@ -153,8 +157,8 @@ convenience for testing.
153157
.SH USAGE
154158
\fB1.\fR Run script or bind to keystroke combination
155159

156-
\fB2.\fR Enter server URL (default vault.bitwarden.com) and login email if not
157-
entered into config.ini already.
160+
\fB2.\fR Enter server URL (default vault.bitwarden.com), login email and 2FA
161+
type if not entered into config.ini already.
158162

159163
\fB3.\fR Start typing to match entries.
160164

bwm/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@
3737
CONF.set('dmenu_passphrase', 'obscure', 'True')
3838
CONF.set('dmenu_passphrase', 'obscure_color', '#222222')
3939
CONF.add_section('vault')
40-
CONF.set('vault', 'server_1', 'https://vault.bitwarden.com')
40+
CONF.set('vault', 'server_1', '')
4141
CONF.set('vault', 'email_1', '')
42+
CONF.set('vault', 'twofactor_1', '')
4243
CONF.set('vault', 'session_timeout_min ', str(SESSION_TIMEOUT_DEFAULT_MIN))
4344
CONF.set('vault', 'autotype_default', SEQUENCE)
4445
CONF.write(conf_file)

bwm/bwcli.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,21 @@ def set_server(url="https://vault.bitwarden.com"):
3939
return True
4040

4141

42-
def login(email, password):
42+
def login(email, password, method=None, code=""):
4343
"""Initial login to Bitwarden Vault.
4444
45+
Args: email - string
46+
password - string
47+
method - int (0: Authenticator, 1: Email, 3: Yubikey)
48+
code - OTP code
49+
4550
Returns: session (bytes) or False on error, Error message
4651
4752
"""
48-
res = run(["bw", "login", "--raw", email, password], capture_output=True, check=False)
53+
cmd = ["bw", "login", "--raw", email, password]
54+
if method and code:
55+
cmd = ["bw", "login", "--raw", email, password, "--method", method, "--code", code]
56+
res = run(cmd, capture_output=True, check=False)
4957
if not res.stdout:
5058
logging.error(res)
5159
return (False, res.stderr)

bwm/bwm.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
import bwm
1818

1919

20-
def get_passphrase():
20+
def get_passphrase(twofac=False):
2121
"""Get a vault password from dmenu or pinentry
2222
23-
Returns: string
23+
Args: twofac - bool (default False) prompt for 2FA code
24+
Returns: string
2425
2526
"""
2627
pinentry = None
28+
pin_prompt = 'setdesc Enter vault password\ngetpin\n' if twofac is False \
29+
else 'setdesc Enter 2FA code\ngetpin\n'
2730
if bwm.CONF.has_option("dmenu", "pinentry"):
2831
pinentry = bwm.CONF.get("dmenu", "pinentry")
2932
if pinentry:
@@ -32,13 +35,13 @@ def get_passphrase():
3235
capture_output=True,
3336
check=False,
3437
encoding=bwm.ENC,
35-
input=b'setdesc Enter vault password\ngetpin\n').stdout
38+
input=pin_prompt).stdout
3639
if out:
3740
res = out.split("\n")[2]
3841
if res.startswith("D "):
3942
password = res.split("D ")[1]
4043
else:
41-
password = dmenu_select(0, "Password")
44+
password = dmenu_select(0, "Password" if twofac is False else "2FA Code")
4245
return password
4346

4447

@@ -58,6 +61,9 @@ def get_vault():
5861
idx = srv.rsplit('_', 1)[-1]
5962
email = args_dict.get(f'email_{idx}', "")
6063
passw = args_dict.get(f'password_{idx}', "")
64+
twofactor = args_dict.get(f'twofactor_{idx}', None)
65+
if not args_dict[srv] or not email:
66+
continue
6167
try:
6268
cmd = args_dict[f'password_cmd_{idx}']
6369
res = subprocess.run(shlex.split(cmd),
@@ -71,8 +77,7 @@ def get_vault():
7177
passw = res.stdout.rstrip('\n') if res.stdout else passw
7278
except KeyError:
7379
pass
74-
if srv:
75-
vaults.append((args_dict[srv], email, passw))
80+
vaults.append((args_dict[srv], email, passw, twofactor))
7681
if not vaults or (not vaults[0][0] or not vaults[0][1]):
7782
res = get_initial_vault()
7883
if res:
@@ -85,7 +90,7 @@ def get_vault():
8590
vaults = [i for i in vaults if i[0] == sel]
8691
if not sel or not vaults:
8792
return None
88-
url, email, passw = vaults[0]
93+
url, email, passw, twofactor = vaults[0]
8994
status = bwcli.status()
9095
if status is False:
9196
return None
@@ -98,7 +103,8 @@ def get_vault():
98103
if res is False:
99104
return None
100105
if status['userEmail'] != email or status['status'] == 'unauthenticated':
101-
session, err = bwcli.login(email, passw)
106+
code = get_passphrase(True) if twofactor else ""
107+
session, err = bwcli.login(email, passw, twofactor, code)
102108
elif status['status'].endswith('locked'):
103109
session, err = bwcli.unlock(passw)
104110
if session is False:
@@ -111,17 +117,21 @@ def get_initial_vault():
111117
"""Ask for initial server URL and email if not entered in config file
112118
113119
"""
114-
url = dmenu_select(0, "Enter server URL.")
120+
url = dmenu_select(0, "Enter server URL.", "https://vault.bitwarden.com")
115121
if not url:
116122
dmenu_err("No URL entered. Try again.")
117123
return False
118124
email = dmenu_select(0, "Enter login email address.")
125+
twofa = {'None': '', 'TOTP': 0, 'Email': 1, 'Yubikey': 3}
126+
method = dmenu_select(len(twofa), "Select Two Factor Auth type.", "\n".join(twofa))
119127
with open(bwm.CONF_FILE, 'w', encoding=bwm.ENC) as conf_file:
120128
bwm.CONF.set('vault', 'server_1', url)
121129
if email:
122130
bwm.CONF.set('vault', 'email_1', email)
131+
if method:
132+
bwm.CONF.set('vault', 'twofactor_1', str(twofa[method]))
123133
bwm.CONF.write(conf_file)
124-
return (url, email, '')
134+
return (url, email, '', twofa[method])
125135

126136

127137
def dmenu_view(entries, folders):

config.ini.example

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# email_1 = <login email>
1717
# password_1 = vault password **INSECURE**
1818
# password_cmd_1 = <command to generate vault password>
19+
# twofactor_1 = <0 for totp, 1 for email or 3 for yubikey>
1920
# server_2 = <url>
2021
# email_2 = <login email>
2122
# etc....

0 commit comments

Comments
 (0)