I wrote some code to use the 1Password CLI
Every time I store an API token in a plaintext file or an environment variable, it creates a lingering annoyance that follows me around whenever I go. Every year or two, another one of these lands on the pile. I am finally working on purging them all. I’m doing it with the 1Password CLI, and so far so good.
op
1Password’s CLI is op
, which lets you do many, many different things. I was
only concerned with two: It lets you read single fields from the vault and it
lets you read entire items. For example, take this login:
You can see there are a bunch of fields, like username
and password
and
website
. You can fetch all of them or just one. It’s a little weird, but
it’s much easier to get a locator for one field than for the whole item. If
you click the “Copy Secret Reference” option, you’ll get something like this on
your clipboard:
"op://rjbs/Pobox/password"
You can pass that URL to op read
and it will print out the value of the
field. Here, that’s the password. Getting one field at a time can be useful
if you only need to retrieve a password or TOTP secret or API token. Often,
though, you’ll want to get the whole login at once. It would mean you could
just store the item’s id rather than a cleartext username and a reference to
the password field. Or worse, a reference to the password field and another
one to the the TOTP field. Also, since each field needs to be retrieved
separately with op read
, it means more external processes and more
possibility of weird errors.
The op item get
command can fetch an entire item with all its fields. It can
spit the whole item out as JSON. Here’s a limited subset of such a document:
{
"fields": [
{
"id": "password",
"type": "CONCEALED",
"purpose": "PASSWORD",
"label": "password",
"value": "eatmorescrapple",
"reference": "op://rjbs/Pobox/password",
"password_details": {
"strength": "DELICIOUS"
}
}
]
}
Unfortunately, 1Password doesn’t make it trivial to get the argument you need
to pass op item get
, but it’s not really hard. You can use “Copy Private
Link”, which will get you a URL something like this (line breaks introduced by
me):
https://start.1password.com/open/i?a=XB4AE5Q2ESODUTKETZB3BQGCM4
&v=flk3x357inyiw22qpoiubhsgin
&i=7wdr3xyzzym2xgorp4zx22zq3h
&h=example.1password.com
The i=
parameter is the item’s id. You can use that as the argument to op
item get
. Alternatively, given the URL like op://rjbs/Pobox/password
you
can extract the vault name (“rjbs”) and the item name (“Pobox”) and pass those
as separate parameters that will be used to search for the item.
But why do either? You can just use Password::OnePassword::OPCLI!
Password::OnePassword::OPCLI
Here are two tiny examples of its use:
my $one_pw = Password::OnePassword::OPCLI->new;
# Get the string found in one field in your 1Password storage:
my $string = $one_pw->get_field("op://rjbs/Pobox/password");
# Get the complete document for an item, as a hashref:
my $pw_item = $one_pw->get_item("7wdr3xyzzym2xgorp4zx22zq3h");
Hopefully by now you can imagine what this is all doing. get_item
returns
the data structure that you’d get from op item get
. You can look at its
fields
entry and find what you need. It does have one other trick worth
mentioning. Because it’s a bit annoying to get the unique identifier for an
item id, you can pass one of those op://
URLs, dropping off the field name,
like this:
# Get the complete document for an item, as a hashref:
my $pw_item = $one_pw->get_item("op://rjbs/Pobox");
I’m currently imagining a world where I stick those URLs in place of API tokens
and make my software smart enough to know that if it’s given an API token that
string starts with op://
, it should treat it as a 1Password reference. I
haven’t implemented everything I need for that, but I did write something to
use this with Dist::Zilla
Dist::Zilla and 1Password
The first thing I wanted to use all this for was my PAUSE password. Unfortunately for me, this was sort of complicated. Or, if not complicated, just tedious. I made a few false starts, but I’m just going to describe the one that I’m running with.
Dist::Zilla is the tool I use (and wrote) for making releases of my CPAN distributions. It’s usually configured with an INI file, like this one:
name = Test-BinaryData
author = Ricardo Signes <cpan@semiotic.systems>
license = Perl_5
copyright_holder = Ricardo Signes
copyright_year = 2010
[@RJBS]
perl-window = long-term
Each section (the things in [...]
) is a plugin of some sort. If the name
starts with an @
it’s a bundle of plugins instead. But there’s another less
commonly seen sigil for plugins: %
. A percent sign means that the thing
being loaded isn’t a plugin but a stash, which holds data for other plugins
to use. These will more often be in ~/.dzil/config.ini
than in each project.
The UploadToCPAN
plugin, which actually uploads tarballs to the CPAN, looks
in a few places for your credentials:
- the
%PAUSE
stash (or another stash of your choosing) ~/.pause
, where CPAN::Uploader usually puts these credentials- user input when prompted
The %PAUSE
stash was slightly overspecified in the code. It had to be a bit
of configuration with the username and passwords given as text. What I did was
relax that so that any stash implementing the (long-existing) Login role could
be used. Then I wrote a new implementation of that role,
Dist::Zilla::Stash::OnePasswordLogin.
In that version of the stash, you only need to provide an item locator, and it
will look up the username and password just in time. So, I have something like
this in my global config now:
[%OnePasswordLogin / %PAUSE]
item = op://rjbs/PAUSE
Who cares if somebody steals this URL? They can’t read the credential unless I
authenticate with 1Password at the time of reading. Putting other login
credentials into your configuration for other plugins is similarly safe. Now,
when I run dzil release
, at the end I’m prompted to touch the fingerprint
scanner to finish releasing. Not only is it more secure, but it feels very
slightly like I’m in some kind of futuristic hacker movie.
What more could I want from my life as a computer programmer?