Skip to content

Commit

Permalink
account-command: Mac OSX Keychain support
Browse files Browse the repository at this point in the history
An implementation to retrieve user credentials from the Mac OSX
Keychain.

A small Python 3 wrapper program to access the Keychain via the
`security` command. Details for setting up NeoMutt to use this program
are contained in `contrib/account-command/README.md`.
  • Loading branch information
the-x-at authored and flatcap committed Jun 3, 2022
1 parent f1a4796 commit c4efb73
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 1 deletion.
3 changes: 2 additions & 1 deletion contrib/Makefile.autosetup
@@ -1,4 +1,4 @@
CONTRIB_DIRS= colorschemes hcache-bench keybase logo lua oauth2 samples vim-keys
CONTRIB_DIRS= colorschemes hcache-bench keybase logo lua oauth2 samples vim-keys account-command

all-contrib:
clean-contrib:
Expand All @@ -13,6 +13,7 @@ install-contrib:
done \
done
chmod +x $(DESTDIR)$(docdir)/keybase/*.sh
chmod +x $(DESTDIR)$(docdir)/account-command/*.py

uninstall-contrib:
for d in $(CONTRIB_DIRS); do \
Expand Down
111 changes: 111 additions & 0 deletions contrib/account-command/README.md
@@ -0,0 +1,111 @@
# Introduction

NeoMutt provides a general mechanism to retrieve password information rather
than storing these sensitive data in the config files using `imap_pass`,
`smtp_pass`, and other `*_pass` variables.

# Supported password managers

- Mac OSX Keychain (Python 3 required)

## Mac OSX Keychain

### Prerequisites

- Python 3

- `security` program located in `/usr/bin`

### Setup

The OSX Keychain can be accessed via the GUI or via the `security` program.
Information displayed in the GUI (Applications > Utilities > Keychain Access)
is rather limited, so we are going to use the command line most of the time.

Generally, any item we want to store in the Keychain is associated with a
server name and an account name. We also give that combination a label, such
that we can easily find it in Keychain Access GUI later.

Specifically, let's assume we want to setup IMAP with SSL/TLS for account
`me@example.com`, which has its IMAP server at `imap.example.com`. The
following command will add this account to the Keychain:

```
$ security add-internet-password -l imap-example -a me@example.com \
-s imaps://imap.example.com
```

Here, `-l` specifies a user-defined label which will be listed as `Name` in
the Keychain Access GUI; `-a` is the user name; and `-s` defines the server
name string. Please note the use of `imaps://` in the definition of this
string, which is generally composed of `protocol://server_name`. The string
for `protocol` is one of NeoMutt's supported account types, `imap`, `pop`,
`smtp`, `nntp` and with a `s` added in case of SSL/TLS use.

The password could be supplied with another option, `-w`. Be aware if this is
done, the cleartext password will be stored in your `~/.bash_history` file.
So instead use the Keychain Access utility and search your newly created
account name (supplied with option `-l`, here it is `imap-example`),
double-click that item in the list and enter your password in the dialog.

Similarly, if sending email is done via host `smtp.example.com`, the following
command will add the corresponding account to the Keychain:

```
$ security add-internet-password -l smtp-example -a me@example.com \
-s smtps://smtp.example.com
```

Again, the password is better entered via the Keychain Access GUI.

You can test that you have successfully entered your credentials in multiple
ways.

```
$ security find-internet-password -g -l imap-example
```

and

```
$ security find-internet-password -g -a me@example.com \
-s imaps://imap.example.com
```

will cause the `security` program to query the passwords in the Keychain.
You will be asked your login password to unlock the Keychain. At this dialog
you can decide if you want to allow this once ("Allow") or if you accept to
not be asked in the future again ("Always Allow"). Both commands will dump a
set of Keychain attributes with `password:` somewhere at the end of the output
(option `-g`).

The `keychain.py` program can be tested accordingly, it will output lines
containing `username: `, `login: `, `password: ` for the associated account.

```
$ ./keychain.py --hostname imap.example.com --username me@example.com \
--type imaps
```

The same commands work with the `smtps` account, too.

In your NeoMutt configuration file, be sure to remove any `*_pass` variables
and set your folder and smtp URLs accordingly, e.g.:

```
set folder = "imaps://me@example.com@imap.example.com"
set smtp_url = "smtps://me@example.com@smtp.example.com"
set account_command = "/usr/share/doc/neomutt/account-command/keychain.py"
```

From now on, your passwords will be queried through the OSX Keychain.

### Considerations

If for convenience you "Always Allow" the `security` program to access the
Keychain, any other program under your system's account will be able to run
`security` and retrieve Keychain data without any further checks.

If you want to be asked for passwords again, lock the Keychain in Keychain
Access. This is also useful if you once pressed "Deny" when `security` wanted
to retrieve data from the Keychain.
82 changes: 82 additions & 0 deletions contrib/account-command/keychain.py
@@ -0,0 +1,82 @@
#!/usr/bin/env python3

import os
import re
import sys
import subprocess
from argparse import ArgumentParser

SECURITY_PROG = "/usr/bin/security"

def emit(msg, error = False, quit = False):
msgType = "Error" if error else "Warning"
sys.stderr.write(msgType + ": " + msg + "\n")
if( quit ):
sys.exit(1)

def getKeychainData(user, host, protocol):
"""Get data from the Keychain data structure.
user account name to query in Keychain (-a flag in security program)
host server name to query in Keychain (-s flag in security program)
protcol protocol name. Used for generating the server name query string,
as 'protocol'://'host'.
"""
params = {
"security": SECURITY_PROG,
"server": protocol + "://" + host,
"account": user,
"keychain": os.path.expanduser("~/Library/Keychains/login.keychain")
}

cmdStr = "{security} find-internet-password -g -a {account} -s {server} "+\
"{keychain}"
cmd = cmdStr.format(**params)
try:
output = subprocess.check_output(cmd, shell=True,
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return None

output = output.decode("utf-8")
match = re.search('password: "(.*)"', output)
if( match ):
return {"username": user,
"password": match.group(1),
"login": user}
else:
return None

KNOWN_PROTOCOLS = ["imap", "imaps", "pop", "pops", "smtp", "smtps", "nntp",
"nntps"]

ap = ArgumentParser(description="NeoMutt script for Mac OSX to retrieve " +
"account credentials from the Keychain using `security` program.")
ap.add_argument("--hostname", metavar = "host", required = True,
action = "store", help = "hostname associated with account.")
ap.add_argument("--username", metavar = "user", required = True,
action = "store", help = "username associated with account.")
ap.add_argument("--type", metavar = "t", required = True,
action = "store", help = "protocol type. Any of the " +
"following: " + ", ".join(KNOWN_PROTOCOLS) + ".")

todo = ap.parse_args()

if( todo.type not in KNOWN_PROTOCOLS ):
emit("Unknown protcol type: `" + todo.type + "'.", error = True,
quit = True)

if( not (os.path.isfile(SECURITY_PROG) or os.path.islink(SECURITY_PROG)) ):
emit("{} program not found.".format(SECURITY_PROG), error = True,
quit = True)
if( not os.access(SECURITY_PROG, os.X_OK) ):
emit("{} is not executable.".format(SECURITY_PROG), error = True,
quit = True)

credentials = getKeychainData(todo.username, todo.hostname, todo.type)
if( credentials is not None ):
print("""username: {username}
login: {login}
password: {password}""".format(**credentials))
else:
emit("No suitable data found in Keychain.", quit = True, error = True)

0 comments on commit c4efb73

Please sign in to comment.