sopass command line password manager

2025-01-26 09:37

Table of Contents

1 Introduction

sopass manages passwords on the command line using the stateless OpenPGP interface (SOP) for encryption. It is a re-interpretation of the concept championed by pass, but using SOP. It does not try to be compatible with pass, only to fit in the same ecological niche.

pass, also known as "passwordstore" and described as "the standard unix password manager" is a command line password manager that uses GnuPG for encryption. sopass prefers SOP to avoid a lock-in to a specific implementation.

1.1 Example

This section gives a taste of using sopass. We first initialize the password store. This creates an empty store as ~/.local/state/sopass/passwords.sopass, and also copies the key file to that directory so that later invocations of sopass find it.

$ sopass init --name mine --key my-openpgpg.key

We can now add a value and show it.

$ echo my secret password | sopass value add --text my/password
$ sopass value show my/password
my secret password
$

We can list all passwords in the store.

$ sopass value list
my/password
$

Finally, we can remove a value.

$ sopass value remove my/password
$ sopass value list
$

2 Software architecture

sopass is a command line tool that runs a SOP implementation as a sub-process, but does not otherwise interact with other software. It stores key/value pairs in a password store, which is a JSON file that has been encrypted with OpenPGP using an implementation of the SOP interface. The clear text data is never stored on persistently. The OpenPGP key is stored next to the encrypted store. Whenever data is accessed, the store is decrypted using the key. Whenever data is modified, the store is re-encrypted using the key.

Some justifications for the architecture:

  • "command line tool"
    • this is inherent in the purpose of the tool
  • "SOP as a sub-process"
    • we want to avoid locking in the user of sopass to a specific implementation of cryptography, so we use the SOP interface to access the implementation we use
  • "does not interact with other software"
    • there is no daemon, background process, online service, or other subsystem that sopass uses, because we want to keep it simple to deploy: only the sopass binary, the store file and the OpenPGP key are needed
  • "JSON"
    • JSON is flexible, well understood, widely supported, and quite sufficient for this use
    • JSON is not great at storing large amounts of binary data, but we don't expect sopass to need to do that; if such a need arises later, it's easy enough to change the clear text format to anything supported by the Rust serde ecosystem
  • "OpenPGP"
    • OpenPGP is criticized and debated, but works well enough, and the SOP interface makes it easy to use
    • we don't expect to change away from OpenPGP or SOP
  • "key stored next to store"
    • this is simplicity for user: the statelessness of SOP means the key location needs to be specified explicitly, and it isn't implicit the way GnuPG does; this is a big part of why SOP is nicer to use from other programs than GnuPG is
  • "never stored persistently"
    • this reduces the likelihood of accidentally leaking secrets in clear text
    • sopass exchanges data with the SOP implementation via Unix pipes

Some compromises made at this stage development:

  • keys without passphrases
    • it's simpler to deal with such keys, as it avoids needing machinery to ask the user for a passphrase
    • this will change, as it's obviously an unacceptable compromise
    • we also aim to support keys backed by hardware modules such as trusted platform or OpenPGP cards
  • store is only encrypted
    • this is for simplicity
    • we will change this so that the store is also always signed so
  • single encryption key
    • for simplicity
    • we will change this so that the store will store all the certificates that it should be encrypted for, similar to the .gpg-id file in the pass store
  • no Git support
    • we will add support to automatically commit a modified store to Git, if the store directory is version controlled with Git; this is similar to what pass does
    • we will also make it easy to manage the Git repository via sopass to mimic pass more
  • no configuration file
    • this is temporary, for simplicity
    • we will add a configuration file to specify things like store and key location

3 Acceptance criteria

This chapter documents explicit acceptance criteria for sopass, and how we verify that the implementation meets them. The verification is done using "scenarios", and the Subplot software turns those into executable code. Running the code verifies the implementation.

3.1 Data files

3.1.1 Pre-generated keys

This is a pre-generated key. We want to avoid generating a new key for each test run, for speed.

-----BEGIN PGP PRIVATE KEY BLOCK-----

xVgEZ2+sZBYJKwYBBAHaRw8BAQdAesm4pX4NYlVa3XUImN1VoGYqlV5wrc+0ChK2
nHQJbagAAQCNaRb0BIm+FvnSehQ+eiGlYt7XkHQEpstH0h6IaMIrsg+PzQpsaXdA
bGl3LmZpwo8EEBYIADcCGQEFAmdvrGQCGwMICwkIBwoNDAsFFQoJCAsCFgIBJxYh
BD3Ix4Uvg4E82hngYARnFaLt6wu0AAoJEARnFaLt6wu03aABANDExR2u4LmA1Ibb
DtTyQnxieLRvVeucpgIWIyR6N6i8AP0Uh6M/yIPJKf9+TPXzyX/pW2hPqFgX4Eqt
XzFP/OrBCcddBGdvrGQSCisGAQQBl1UBBQEBB0DU+xruTz4AOT0hsLSu6Ji5Onkq
xCKtzdW7upJKzInEWAMBCAcAAP9m/VjK+jQOvMF8aBBthpE/nU44qkHwTlORSTFl
anrreBBKwngEGBYIACAFAmdvrGQCGwwWIQQ9yMeFL4OBPNoZ4GAEZxWi7esLtAAK
CRAEZxWi7esLtL42AP9maHm227K9/V68GOLXLgykAwlwqhx7KKbAcyRJPOhq5gEA
z28qv1cXrs0ctv3VAqsMd/OTV+ODDFvwOVvNdDZuYQ0=
=AYJk
-----END PGP PRIVATE KEY BLOCK-----

This is a second pre-generated key.

-----BEGIN PGP PRIVATE KEY BLOCK-----
Comment: BC2B ADD9 3C89 D080 8E98  5A1F 8962 5382 6F49 4D3A

xVgEZ3gPORYJKwYBBAHaRw8BAQdARrDdx/wzj8P9F40LJWYpT3wdy9yLvf1U069q
vc6M4M4AAQCwrh2W3cRSGCCciDWgMo210pLeq3Os/faaFXaPslu+pRHNwsALBB8W
CgB9BYJneA85AwsJBwkQiWJTgm9JTTpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu
c2VxdW9pYS1wZ3Aub3JnCkala0J2+OcEzzc7NfBJGv0HsiACp/2nWQe54bOlwZUD
FQoIApsBAh4BFiEEvCut2TyJ0ICOmFofiWJTgm9JTToAABnwAP9rn+AciFLaN/Up
leuz6jVhRfYG33IGj13n35rvs4qm+gEA3uHf9y5T9emmEv95MhhazGlp3xCugki/
i9WkEhTEvALHWARneA85FgkrBgEEAdpHDwEBB0CWv7+ruWlq8GTIo2QG/ycWJMz7
mFBQS9Uibyv7RiB4CAAA/igjj1gIsn4lP5TfzK4aQhx9arUI5b5qYFjgUVCAO3EG
Dk3CwL8EGBYKATEFgmd4DzkJEIliU4JvSU06RxQAAAAAAB4AIHNhbHRAbm90YXRp
b25zLnNlcXVvaWEtcGdwLm9yZ5Z1FkiL1r/pJIlRW3bogHpL5xt010SBHQWC6eR+
jx3xApsCvqAEGRYKAG8Fgmd4DzkJEA5YzpG5RD+ARxQAAAAAAB4AIHNhbHRAbm90
YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+CQJbbTcTmFrGiLf4ts77eIQ9rYMljuvzwY
p/iEYJzgFiEEaZkggLtCLYWSMdahDljOkblEP4AAAPeXAP9C3R7Hn0jqm+xsC2Ym
Pv+H4d3eRtfndDCQTR2p3bJwkAD+IRMUXNS/Hg2zoVY4Y6LJMz6sdzi4dgzoUo+q
I1tkQAAWIQS8K63ZPInQgI6YWh+JYlOCb0lNOgAAJz8A/jIRrHZZ6lt9LecBzc+Y
7sG75m1XlvY/b8FEhE2ac9bqAQCyK4FVI1tmYmQ2Ji+wMwyRQ6iK8Brd85GSIcFI
FVfmBMddBGd4DzkSCisGAQQBl1UBBQEBB0Cr2zlOc4zZQiYg8gIQTBZX4xAJKTin
0JFL8ttuUcLNDAMBCAcAAP9qOp+iWiZMDuekZ8jdQC802NVZZXIe9JNK0YMp+Wc7
kBBswsAABBgWCgByBYJneA85CRCJYlOCb0lNOkcUAAAAAAAeACBzYWx0QG5vdGF0
aW9ucy5zZXF1b2lhLXBncC5vcmf6TAclvwInoVzGCDeyXLkdwBQ4zRdcr+KAXnIW
P1CwuAKbDBYhBLwrrdk8idCAjphaH4liU4JvSU06AAAE0wD/eCxv/xXYqgju+noA
a/VAyUkJsRGHkLPyK8YX1r565G8BAOpeUkkWeAyOJLVXyZ650xJkoEXvLz3+XGft
fU5QwskF
=lebt
-----END PGP PRIVATE KEY BLOCK-----

The certificate extracted from other.key:

-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: BC2B ADD9 3C89 D080 8E98  5A1F 8962 5382 6F49 4D3A

xjMEZ3gPORYJKwYBBAHaRw8BAQdARrDdx/wzj8P9F40LJWYpT3wdy9yLvf1U069q
vc6M4M7CwAsEHxYKAH0Fgmd4DzkDCwkHCRCJYlOCb0lNOkcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcKRqVrQnb45wTPNzs18Eka/QeyIAKn
/adZB7nhs6XBlQMVCggCmwECHgEWIQS8K63ZPInQgI6YWh+JYlOCb0lNOgAAGfAA
/2uf4ByIUto39SmV67PqNWFF9gbfcgaPXeffmu+ziqb6AQDe4d/3LlP16aYS/3ky
GFrMaWnfEK6CSL+L1aQSFMS8As4zBGd4DzkWCSsGAQQB2kcPAQEHQJa/v6u5aWrw
ZMijZAb/JxYkzPuYUFBL1SJvK/tGIHgIwsC/BBgWCgExBYJneA85CRCJYlOCb0lN
OkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeWdRZIi9a/
6SSJUVt26IB6S+cbdNdEgR0Fgunkfo8d8QKbAr6gBBkWCgBvBYJneA85CRAOWM6R
uUQ/gEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfgkCW2
03E5haxoi3+LbO+3iEPa2DJY7r88GKf4hGCc4BYhBGmZIIC7Qi2FkjHWoQ5YzpG5
RD+AAAD3lwD/Qt0ex59I6pvsbAtmJj7/h+Hd3kbX53QwkE0dqd2ycJAA/iETFFzU
vx4Ns6FWOGOiyTM+rHc4uHYM6FKPqiNbZEAAFiEEvCut2TyJ0ICOmFofiWJTgm9J
TToAACc/AP4yEax2WepbfS3nAc3PmO7Bu+ZtV5b2P2/BRIRNmnPW6gEAsiuBVSNb
ZmJkNiYvsDMMkUOoivAa3fORkiHBSBVX5gTOOARneA85EgorBgEEAZdVAQUBAQdA
q9s5TnOM2UImIPICEEwWV+MQCSk4p9CRS/LbblHCzQwDAQgHwsAABBgWCgByBYJn
eA85CRCJYlOCb0lNOkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBn
cC5vcmf6TAclvwInoVzGCDeyXLkdwBQ4zRdcr+KAXnIWP1CwuAKbDBYhBLwrrdk8
idCAjphaH4liU4JvSU06AAAE0wD/eCxv/xXYqgju+noAa/VAyUkJsRGHkLPyK8YX
1r565G8BAOpeUkkWeAyOJLVXyZ650xJkoEXvLz3+XGftfU5QwskF
=Gwpf
-----END PGP PUBLIC KEY BLOCK-----

3.2 Reports its version

Want: The sopass program reports its version when requested, and it's in the format used my semantic versioning.

Why: This is partly a smoke test: if this doesn't work, we can't expect anything else to work either. But it's also useful in situations when someone else is using sopass and we want to know what version they have.

Common current Unix practice is to have a --version option, so we support that, but we also support a version subcommand, as we have an interface based on subcommands.

given an installed sopass
when I run sopass --version
then stdout matches regex ^sopass \d+\.\d+\.\d+$
when I run sopass version
then stdout matches regex ^sopass \d+\.\d+\.\d+$

3.3 Reports a default configuration

Want: The sopass program reports its default configuration when requested.

Why: This is useful so that users can see what the configuration is, even if they haven't set on themselves.

given an installed sopass
when I run sopass config
then stdout contains ""store": "/"
then stdout contains ""sop": ""
then stdout contains ""key_file": ""

3.4 Loads default configuration file

Want: The sopass program loads its default configuration when it exists.

Why: This is useful so that users don't need to always specify which configuration file to use.

given an installed sopass
given file .config/sopass/sopass.yml from default.yaml
when I run env HOME=. sopass config
then stdout contains ""store": "/over/the/rainbow""
store: /over/the/rainbow

3.5 Loads specified configuration file

Want: The sopass program loads a configuration file requested by user.

Why: This is useful so that user can use a non-default configuration file.

given an installed sopass
given file custom.yaml
when I run sopass --config custom.yaml config
then stdout contains ""sop": "soppy""
sop: soppy

3.6 Initializes the password store

Want: The program initializes the password store.

Why: This is fundamental to how we want the software to be used.

given an installed sopass
given file my.key
then directory xyzzy does not exist
when I run sopass --sop sqop --store xyzzy init --name primary --key my.key
then file xyzzy/values.sopass exists

3.7 Manages values

Want: The user can add and remove a value and list all values.

Why: This is fundamental for the purpose of the software.

given an installed sopass
given file my.key
when I run sopass --sop sqop --store xyzzy init --name primary --key my.key
when I run sopass --sop sqop --store xyzzy value list
when I run sopass --sop sqop --store xyzzy value add foo bar
when I run sopass --sop sqop --store xyzzy value list
then stdout is exactly "foo "
when I run sopass --sop sqop --store xyzzy value show foo
then stdout is exactly "bar "
when I run sopass --sop sqop --store xyzzy value remove foo
when I run sopass --sop sqop --store xyzzy value list
then stdout is exactly ""

3.8 Showing value that does not exist fails

What: Trying to show a value that does not exist in the store fails.

Why: If the command doesn't fail, the user may think the value is the empty string.

given an installed sopass
given file my.key
when I run sopass --sop sqop --store xyzzy init --name primary --key my.key
when I try to run sopass --sop sqop --store xyzzy value show foo
then command fails
then stderr contains "foo"
then stdout is exactly ""

3.9 Renames values

Want: The user can rename a value.

Why: This is very handy.

given an installed sopass
given file my.key
when I run sopass --sop sqop --store xyzzy init --name primary --key my.key
when I run sopass --sop sqop --store xyzzy value add foo bar
when I run sopass --sop sqop --store xyzzy value add foobar bar
when I try to run sopass --sop sqop --store xyzzy value rename ghost yo
then command fails
then stderr contains "ghost"
when I try to run sopass --sop sqop --store xyzzy value rename foo foobar
then command fails
then stderr contains "foobar"
when I run sopass --sop sqop --store xyzzy value rename foo yo
when I run sopass --sop sqop --store xyzzy value list
then stdout is exactly "foobar yo "

3.10 Manages certificates

Want: The password store contains certificates for which to encrypt.

Why: This allows the store to be shared between devices without sharing the encryption key.

given an installed sopass
given file my.key
given file other.key
given file other.cert
when I run sopass --sop sqop --store xyzzy init --name primary --key my.key
when I run sopass --sop sqop --store xyzzy cert list
then stdout is exactly "primary "
when I run sopass --sop sqop --store xyzzy cert add --name secondary --cert other.cert
when I run sopass --sop sqop --store xyzzy cert list
then stdout contains "primary"
then stdout contains "secondary"
when I run mv other.key xyzzy/default.key
when I run rm my.key
when I run sopass --sop sqop --store xyzzy cert list
then stdout contains "primary"
then stdout contains "secondary"