moss

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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:
A.gitignore | 1+
AGemfile | 8++++++++
Afeatures/rjpass.feature | 39+++++++++++++++++++++++++++++++++++++++
Afeatures/step_definitions/steps.rb | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afeatures/support/env.rb | 7+++++++
Afixtures/store/.age/identity | 3+++
Afixtures/store/home/ebay.age | 7+++++++
Afixtures/store/home/fastmail.age | 7+++++++
Afixtures/store/home/yahoomail.age | 7+++++++
Afixtures/store/work/gmail.age | 7+++++++
Agemset.nix | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arjpass.rb | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ashell.nix | 9+++++++++
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"; +}