sopass command line password manager

2025-12-09 15:03

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, as well as a set of certificates. The store is encrypted with OpenPGP using an implementation of the SOP interface. The clear text data is never stored on persistently. The secret OpenPGP key is stored next to the encrypted store or on an OpenPGP card. The encryption is done using all the certificates stored in the store itself. Whenever data is accessed, the store is decrypted using the key. Whenever data is modified, the store is re-encrypted using the stored certificates.

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
  • "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
    • this is now optional: sopass can also an OpenPGP card for the secret key
  • "certificates stored in the store"
    • the store has a list of certificates and the list can be managed with sopass cert
    • the list is boot strapped when the store is initialized: the initialization uses a key and a certificate is extracted from that
  • "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 Configuration file

This uses the directory my.store as the store, rsop for the SOP implem3entation. The configuration file gets install so that the sopass invoked by scenarios use it by default.

store: my.store
sop: rsop
sop_decrypt: rsop

3.1.2 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.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
3 when I run sopass --version
4 then stdout matches regex ^sopass \d+\.\d+\.\d+$
5 when I run sopass version
6 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.

1 given an installed sopass
2 when I run sopass config
3 then stdout contains ""store": "/"
4 then stdout contains ""sop": ""
5 then stdout contains ""key_file": ""
6 then file .config/sopass/sopass.yml does not exist

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.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from default.yaml
3 when I run env HOME=. sopass config
4 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.

1 given an installed sopass
2 when I try to run sopass --config custom.yaml config
3 then command fails
4 then stderr contains "custom.yaml"
5 given file custom.yaml
6 when I run sopass --config custom.yaml config
7 then stdout contains ""sop": "soppy""
sop: soppy

3.6 Rejects unknown field in configuration file

Want: The sopass program refuses to load a configuration file that has an unknown field.

Why: This helps prevent typos in configuration files.

1 given an installed sopass
2 given file unknown.yaml
3 when I try to run sopass --config unknown.yaml config
4 then command fails
5 then stderr contains "unknown"
unknown: true

3.7 Initializes the password store

Want: The program initializes the password store.

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

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
3 given file my.key
4 then directory xyzzy does not exist
5 when I run sopass init --name primary --key my.key
6 then file my.store/values.sopass exists

3.8 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.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
3 given file my.key
 
4 when I run sopass init --name primary --key my.key
5 when I run sopass value list
6 then stdout is exactly ""
 
7 given file value.dat
8 when I run sopass value add foo --file value.dat
9 when I run sopass value list
10 then stdout is exactly "foo "
11 when I run sopass value show foo
12 then stdout is exactly "bar "
13 when I run sopass value remove foo
14 then stdout is exactly ""
 
15 given file add-stdin.sh
16 when I run sh add-stdin.sh
17 when I run sopass value list
18 then stdout is exactly "foo "
19 when I run sopass value show foo
20 then stdout is exactly "bar "
21 when I run sopass value remove foo
22 then stdout is exactly ""
bar
#!/bin/sh
set -x
cat value.dat
ls -l value.dat
sopass value add foo --stdin < value.dat

3.9 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.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
3 given file my.key
 
4 when I run sopass init --name primary --key my.key
5 when I try to run sopass value show foo
6 then command fails
7 then stderr contains "foo"
8 then stdout is exactly ""

3.10 Renames values

Want: The user can rename a value.

Why: This is very handy.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
3 given file my.key
 
4 when I run sopass init --name primary --key my.key
 
5 given file value.dat
6 when I run sopass value add foo --file value.dat
7 when I run sopass value add foobar --file value.dat
 
8 when I try to run sopass value rename ghost yo
9 then command fails
10 then stderr contains "ghost"
 
11 when I try to run sopass value rename foo foobar
12 then command fails
13 then stderr contains "foobar"
 
14 when I run sopass value rename foo yo
15 when I run sopass value list
16 then stdout is exactly "foobar yo "

3.11 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.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
3 given file my.key
4 given file other.key
5 given file other.cert
 
6 when I run sopass init --name primary --key my.key
7 when I run sopass cert list
8 then stdout is exactly "primary "
 
9 when I run sopass cert add --name secondary --cert other.cert
10 when I run sopass cert list
11 then stdout contains "primary"
12 then stdout contains "secondary"
 
13 when I run mv other.key my.store/default.key
14 when I run rm my.key
15 when I run sopass cert list
16 then stdout contains "primary"
17 then stdout contains "secondary"
 
18 when I run sopass cert remove primary
19 when I run sopass cert list
20 then stdout doesn't contain "primary"
21 then stdout contains "secondary"
 
22 when I try to run sopass cert remove secondary
23 then command fails
24 then stderr contains "secondary"
25 when I run sopass cert list
26 then stdout contains "secondary"

3.12 Exports store to file

Want: The user can export all contents from store to a JSON file.

Why: This allows moving data to another application or another, incompatible version of sopass itself.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
3 given file my.key
 
4 when I run sopass init --name primary --key my.key
5 given file value.dat
6 when I run sopass value add foo --file value.dat
 
7 when I run sopass export store.json
8 when I run cat store.json
9 then file store.json contains ""certs":"
10 then file store.json contains ""primary":"
11 then file store.json contains ""kv":"
12 then file store.json contains ""foo":"

3.13 Imports from a file

Want: The user can import an exported store into another store.

Why: This allows moving data to another application or another, incompatible version of sopass itself.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
 
3 given file my.key
4 given file value.dat
5 given file yo.dat
 
6 when I run sopass init --name primary --key my.key
7 when I run sopass value add foo --file value.dat
8 when I run sopass value add bar --file value.dat
9 when I run sopass value add yo --file value.dat
10 when I run sopass export store.json
 
11 when I run sopass --store new init --name primary --key my.key
12 when I run sopass --store new value add foo --file yo.dat
13 when I run sopass --store new value add bar --file yo.dat
14 when I run sopass --store new import store.json
 
15 when I run sopass --store new value list
16 then stdout contains "foo"
17 then stdout contains "bar"
18 then stdout contains "yo"
 
19 when I run sopass --store new value show foo
20 then stdout is exactly "bar "
 
21 when I run sopass --store new value show bar
22 then stdout is exactly "bar "
 
23 when I run sopass --store new value show yo
24 then stdout is exactly "bar "
yo

3.14 Generates a password

Want: The user can have the program generate a random password

Why: This is a common need.

Note that we do not verify that the generated passwords are actually secure. We rely on the passwords crate to do this correctly.

1 given an installed sopass
2 given file .config/sopass/sopass.yml from config.yaml
3 given file my.key
4 when I run sopass init --name primary --key my.key
5 when I run sopass value generate foo
6 when I run sopass value list
7 then stdout is exactly "foo "
8 when I run sopass value show foo
9 then stdout isn't exactly ""