commit a780a142f8d00d88618e658bb7965259427428c2
Author: Daniel Barlow <dan@telent.net>
Date: Sun, 8 Aug 2021 23:14:30 +0100
initial commit, glimmerings of doing something useful
Diffstat:
13 files changed, 443 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+Gemfile.lock
diff --git a/Gemfile b/Gemfile
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
+
+gem "cucumber"
+gem "rspec"
diff --git a/features/rjpass.feature b/features/rjpass.feature
@@ -0,0 +1,39 @@
+Feature: command-line password management
+
+ Scenario: secrets are encrypted using age
+ Given I am using the example password store
+ Then "home/ebay.age" plaintext is "horsebattery"
+
+ Scenario: I can create a specified secret
+ Given I am using a temporary password store
+ When I store a secret for "home/ebay" with content "horsebattery"
+ Then "home/ebay.age" plaintext is "horsebattery"
+
+ Scenario: I can create a generated secret
+ Given I am using a temporary password store
+ When I generate a secret for "home/ebay" with length 20
+ Then I see a 20 character string
+ And "home/ebay.age" plaintext matches ^[a-zA-Z0-9]{20}$
+ When I generate a secret for "home/fastmail" with length 12
+ And "home/fastmail.age" plaintext matches ^[a-zA-Z0-9]{12}$
+
+ Scenario: I can view a secret
+ Given I am using the example password store
+ When I view the secret "home/ebay"
+ Then I see the string "horsebattery"
+
+ Scenario: I can search for a secret
+ Given I am using the example password store
+ When I search for "mail"
+ Then I see the string "work/gmail"
+ And I see the string "home/yahoomail"
+
+ @wip
+ Scenario: I can edit a secret
+ Given I am using the example password store
+ When I edit work/gmail
+ Then the editor opens with a plaintext file "horsebattery"
+
+ Scenario: Secrets are stored in a git repo
+
+ Scenario: It pushes changes to a git remote
diff --git a/features/step_definitions/steps.rb b/features/step_definitions/steps.rb
@@ -0,0 +1,64 @@
+RJPASS="./rjpass.rb"
+
+require 'tmpdir'
+require 'fileutils'
+
+def shell(s)
+ output = %x{bash -c #{s.inspect}}
+ $?.exitstatus.zero? or raise "#{$?} : #{@i_see}"
+ output
+end
+
+def store_path(s)
+ Pathname.new(ENV["RJPASS_STORE"]).join(s)
+end
+
+Given("I am using a temporary password store") do
+ ENV["RJPASS_STORE"] = Dir.mktmpdir
+ Dir.mkdir(store_path(".age"))
+ FileUtils.cp("fixtures/store/.age/identity", store_path(".age/identity"))
+end
+
+Given("I am using the example password store") do
+ ENV["RJPASS_STORE"] = "fixtures/store"
+end
+
+When("I generate a secret for {string} with length {int}") do |name, length|
+ @i_see = shell "#{RJPASS} generate #{name} #{length}"
+end
+
+When("I store a secret for {string} with content {string}") do |name, content|
+ shell "echo -n #{content} | #{RJPASS} insert #{name}"
+end
+
+Then("I see a {int} character string") do |len|
+ expect(@i_see.length).to eq len
+end
+
+Then("I see the string {string}") do |s|
+ expect(@i_see).to match(s)
+end
+
+Then("{string} plaintext is {string}") do |name, expected|
+ path_name = store_path(name)
+ contents = File.read(path_name)
+ expect(contents).to match /AGE ENCRYPTED FILE/
+ plaintext = shell "age -i #{store_path(".age/identity")} -d #{path_name}"
+ expect(plaintext).to eq expected
+end
+
+Then("{string} plaintext matches {word}") do |name, re|
+ path_name = store_path(name)
+ contents = File.read(path_name)
+ expect(contents).to match /AGE ENCRYPTED FILE/
+ shell "age -i #{store_path(".age/identity")} -d #{path_name}"
+ expect(@i_see).to match Regexp.new(re)
+end
+
+When("I view the secret {string}") do |name|
+ @i_see = shell "#{RJPASS} cat #{name}"
+end
+
+When("I search for {string}") do |term|
+ @i_see = shell "#{RJPASS} search #{term}"
+end
diff --git a/features/support/env.rb b/features/support/env.rb
@@ -0,0 +1,7 @@
+require 'rspec/core'
+
+RSpec.configure do |config|
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
diff --git a/fixtures/store/.age/identity b/fixtures/store/.age/identity
@@ -0,0 +1,3 @@
+# created: 2021-08-08T17:24:45+01:00
+# public key: age1wc392uhfm04sy5nmpu86ea077ymjxs6e3gnda54jvpmuqchj9d5s6d0q2x
+AGE-SECRET-KEY-1ZALPNKLG5794VS0QVE2Y5P5CSLPPKM6DQULEEMJFZMYCZH8Z3SMSCGWZZ7
diff --git a/fixtures/store/home/ebay.age b/fixtures/store/home/ebay.age
@@ -0,0 +1,7 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDdHJxUjl5ZUorNUxJZHBW
+LzB1TVE4YS9qQm93R3lleGQxK0Z3MjhNWDMwCmEvbjgxK1B3MnBra245OHRDTkMr
+QzZ6SDA1OXZWM3dwd01RdVk2MFVaVkUKLS0tIFUxUWYyazdUVjVkZitKalRTcThs
+cTliNnFvVXFwaWQ1S1RnOFV3K3pMOEEKp37wj6WdTuT1SHUBlQll7e2LL6QVZEUb
+S17aYC9NCBTnRYF2JBgyN8EhBrI=
+-----END AGE ENCRYPTED FILE-----
diff --git a/fixtures/store/home/fastmail.age b/fixtures/store/home/fastmail.age
@@ -0,0 +1,7 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDdHJxUjl5ZUorNUxJZHBW
+LzB1TVE4YS9qQm93R3lleGQxK0Z3MjhNWDMwCmEvbjgxK1B3MnBra245OHRDTkMr
+QzZ6SDA1OXZWM3dwd01RdVk2MFVaVkUKLS0tIFUxUWYyazdUVjVkZitKalRTcThs
+cTliNnFvVXFwaWQ1S1RnOFV3K3pMOEEKp37wj6WdTuT1SHUBlQll7e2LL6QVZEUb
+S17aYC9NCBTnRYF2JBgyN8EhBrI=
+-----END AGE ENCRYPTED FILE-----
diff --git a/fixtures/store/home/yahoomail.age b/fixtures/store/home/yahoomail.age
@@ -0,0 +1,7 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDdHJxUjl5ZUorNUxJZHBW
+LzB1TVE4YS9qQm93R3lleGQxK0Z3MjhNWDMwCmEvbjgxK1B3MnBra245OHRDTkMr
+QzZ6SDA1OXZWM3dwd01RdVk2MFVaVkUKLS0tIFUxUWYyazdUVjVkZitKalRTcThs
+cTliNnFvVXFwaWQ1S1RnOFV3K3pMOEEKp37wj6WdTuT1SHUBlQll7e2LL6QVZEUb
+S17aYC9NCBTnRYF2JBgyN8EhBrI=
+-----END AGE ENCRYPTED FILE-----
diff --git a/fixtures/store/work/gmail.age b/fixtures/store/work/gmail.age
@@ -0,0 +1,7 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsc2xFeVdReDFzaVo2NzhC
+R0RxQVlDYXJZclZ1bnVjN0lwaFVrcEVlcG1ZCmcwbWNvc2F0ZjU2QnFIV3FZdW1q
+MXR1dFRjY0FPR09JWERKV3VqTERnWFEKLS0tICsxZXpPNjhwQ3plQVRxUVNna0J6
+UUo2Z0lBdUt5elhIeFJTeFc5MStuWEkKpjGJbgnPrbDeYH5IqHEG0vTsqOTbDjzo
+JddMLO3xCeq4CxBn7s3tpetRK0jSkQ==
+-----END AGE ENCRYPTED FILE-----
diff --git a/gemset.nix b/gemset.nix
@@ -0,0 +1,224 @@
+{
+ builder = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "045wzckxpwcqzrjr353cxnyaxgf0qg22jh00dcx7z38cys5g1jlr";
+ type = "gem";
+ };
+ version = "3.2.4";
+ };
+ cucumber = {
+ dependencies = ["builder" "cucumber-core" "cucumber-create-meta" "cucumber-cucumber-expressions" "cucumber-gherkin" "cucumber-html-formatter" "cucumber-messages" "cucumber-wire" "diff-lcs" "mime-types" "multi_test" "sys-uname"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1yp24gd7rf5pv6jrag7jdgd2n3sgbz3nfz9hwhs80lfmwjvdviq7";
+ type = "gem";
+ };
+ version = "7.0.0";
+ };
+ cucumber-core = {
+ dependencies = ["cucumber-gherkin" "cucumber-messages" "cucumber-tag-expressions"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "0vc8lxzikx0sxh3f8mdfqwkfp1649fm5lw7mhly5hpfjsbzfwlaq";
+ type = "gem";
+ };
+ version = "10.0.1";
+ };
+ cucumber-create-meta = {
+ dependencies = ["cucumber-messages" "sys-uname"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1i368l09n3dq95666bampzvnbsd2z1381rshcnvjra8d9c6525yi";
+ type = "gem";
+ };
+ version = "6.0.1";
+ };
+ cucumber-cucumber-expressions = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "132yasdhxzaya65qwzkv5hng3sbdr51x97wfh9kj80zszli6qpvp";
+ type = "gem";
+ };
+ version = "12.1.1";
+ };
+ cucumber-gherkin = {
+ dependencies = ["cucumber-messages"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1ivrxg9cwf7lywjj4y5kaqz3273a83w1hgf9v2bpz1dg2k8w8ydn";
+ type = "gem";
+ };
+ version = "20.0.1";
+ };
+ cucumber-html-formatter = {
+ dependencies = ["cucumber-messages"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "0m7h4ahgv7g40ndc86jncvbwclwlsicrsf2cda6z1wnxlyi24xfd";
+ type = "gem";
+ };
+ version = "16.0.1";
+ };
+ cucumber-messages = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1zc484labymlffc243k79835pg6zqm3jjffvsjy6zi4jcwgkmx5s";
+ type = "gem";
+ };
+ version = "17.0.1";
+ };
+ cucumber-tag-expressions = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "0w21jnzqbqzwa24424hj9wcrlysby95ava0hl2zv74a96y6k4df7";
+ type = "gem";
+ };
+ version = "3.0.1";
+ };
+ cucumber-wire = {
+ dependencies = ["cucumber-core" "cucumber-cucumber-expressions" "cucumber-messages"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "06p66mfccak9nskwslnvkac0503q310q7n32imh9q2gknh0iliw5";
+ type = "gem";
+ };
+ version = "6.0.1";
+ };
+ diff-lcs = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "0m925b8xc6kbpnif9dldna24q1szg4mk0fvszrki837pfn46afmz";
+ type = "gem";
+ };
+ version = "1.4.4";
+ };
+ ffi = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1wgvaclp4h9y8zkrgz8p2hqkrgr4j7kz0366mik0970w532cbmcq";
+ type = "gem";
+ };
+ version = "1.15.3";
+ };
+ mime-types = {
+ dependencies = ["mime-types-data"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1zj12l9qk62anvk9bjvandpa6vy4xslil15wl6wlivyf51z773vh";
+ type = "gem";
+ };
+ version = "3.3.1";
+ };
+ mime-types-data = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "0dlxwc75iy0dj23x824cxpvpa7c8aqcpskksrmb32j6m66h5mkcy";
+ type = "gem";
+ };
+ version = "3.2021.0704";
+ };
+ multi_test = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1sx356q81plr67hg16jfwz9hcqvnk03bd9n75pmdw8pfxjfy1yxd";
+ type = "gem";
+ };
+ version = "0.1.2";
+ };
+ rspec = {
+ dependencies = ["rspec-core" "rspec-expectations" "rspec-mocks"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1dwai7jnwmdmd7ajbi2q0k0lx1dh88knv5wl7c34wjmf94yv8w5q";
+ type = "gem";
+ };
+ version = "3.10.0";
+ };
+ rspec-core = {
+ dependencies = ["rspec-support"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "0wwnfhxxvrlxlk1a3yxlb82k2f9lm0yn0598x7lk8fksaz4vv6mc";
+ type = "gem";
+ };
+ version = "3.10.1";
+ };
+ rspec-expectations = {
+ dependencies = ["diff-lcs" "rspec-support"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1sz9bj4ri28adsklnh257pnbq4r5ayziw02qf67wry0kvzazbb17";
+ type = "gem";
+ };
+ version = "3.10.1";
+ };
+ rspec-mocks = {
+ dependencies = ["diff-lcs" "rspec-support"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "1d13g6kipqqc9lmwz5b244pdwc97z15vcbnbq6n9rlf32bipdz4k";
+ type = "gem";
+ };
+ version = "3.10.2";
+ };
+ rspec-support = {
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "15j52parvb8cgvl6s0pbxi2ywxrv6x0764g222kz5flz0s4mycbl";
+ type = "gem";
+ };
+ version = "3.10.2";
+ };
+ sys-uname = {
+ dependencies = ["ffi"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ remotes = ["https://rubygems.org"];
+ sha256 = "0gk625krfm00nppb2ni0794kzr1cqbs1a0059fhp4s3lcrmx69jc";
+ type = "gem";
+ };
+ version = "1.2.2";
+ };
+}
diff --git a/rjpass.rb b/rjpass.rb
@@ -0,0 +1,60 @@
+#!/usr/bin/env ruby
+require 'pathname'
+
+STORE = Pathname.new(ENV['RJPASS_STORE'] || "/tmp/store")
+IDENTITY = "age1wc392uhfm04sy5nmpu86ea077ymjxs6e3gnda54jvpmuqchj9d5s6d0q2x"
+
+def random_alnum(length)
+ bytes = File.open("/dev/urandom", "rb") do |random|
+ random.read(length).unpack("C*").map {|c|
+ c = c % 62
+ case
+ when c < 26
+ ('A'.ord + c)
+ when c < 52
+ ('a'.ord + (c-26))
+ else
+ ('0'.ord + (c-52))
+ end
+ }
+ end
+ bytes.pack("C*")
+end
+
+def write_secret(name, content)
+ pathname = STORE.join("#{name}.age")
+ pathname.dirname.mkpath
+ IO.popen("age -a -r #{IDENTITY} -o #{pathname.to_s}", "w") do |f|
+ f.write(content)
+ end
+end
+
+def read_secret(name)
+ pathname = STORE.join("#{name}.age")
+ File.exist?(pathname) or raise "Can't open #{pathname}: $!"
+ `age -i #{STORE.join(".age/identity")} -d #{pathname}`
+end
+
+
+case ARGV[0]
+when 'generate'
+ file , len = ARGV[1..]
+ secret = random_alnum(len.to_i)
+ print secret
+ write_secret(file, secret)
+when 'insert'
+ file, = ARGV[1..]
+ secret = STDIN.read
+ write_secret(file, secret)
+when 'cat','show'
+ file, = ARGV[1..]
+ STDOUT.write(read_secret(file))
+when 'search'
+ term = ARGV[1..].join(" ")
+ files = Dir[STORE.join('**/*.age')].filter {|f| f.match(Regexp.new(term)) }
+ files.each do |n|
+ puts Pathname.new(n).relative_path_from(STORE).sub_ext('')
+ end
+else
+ raise "command #{ARGV[0]} unrecognized"
+end
diff --git a/shell.nix b/shell.nix
@@ -0,0 +1,9 @@
+with import <nixpkgs> {};
+let gems = bundlerEnv {
+ name = "cuke-gems";
+ gemdir = ./.;
+ };
+in mkShell {
+ packages = [ gems gems.wrappedRuby pkgs.age pkgs.entr ];
+ watch = "find . -type f | entr cucumber --publish-quiet -i";
+}