moss

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

commit 5aa1f9c25aecf99e7257cd1699c9e1b560613d0b
parent 732ad3eca2f550d6b155ba7108140dbfbde06b2e
Author: Daniel Barlow <dan@telent.net>
Date:   Tue, 10 Aug 2021 23:27:54 +0100

support encrypted identity

Diffstat:
Mfeatures/moss.feature | 9++++++++-
Mfeatures/step_definitions/steps.rb | 25++++++++++++++++++++++---
Afixtures/keys/encrypted.key | 0
Amoss-init-with-passphrase.expect | 9+++++++++
Mmoss.rb | 26+++++++++++++++++++++++---
Mshell.nix | 1+
6 files changed, 63 insertions(+), 7 deletions(-)

diff --git a/features/moss.feature b/features/moss.feature @@ -85,4 +85,11 @@ Feature: command-line password management When I create a moss instance with identity "me.key" Then the instance store exists And the instance identity is "me.key" - And the store root contains .recipients for the identity "me.key" + And the store root has .recipients for the identity "me.key" + + Scenario: I can create a new store with an encrypted key + Given I set MOSS_STORE to a unique temporary pathname + When I interactively create a moss instance with identity "encrypted.key" and passphrase "hello foo" + Then the instance store exists + And the instance identity is "encrypted.key" + And the store root has recipient "age1qqawnse9rfxjun2xmklk3r2pudlfjx0z4utjuwlmq7t9l60z9e2sap9vc8" diff --git a/features/step_definitions/steps.rb b/features/step_definitions/steps.rb @@ -25,13 +25,24 @@ def add_to_store(path, content) end def recipient_for_identity(identity_file) - File.open(identity_file).read.match(/(age1.+)$/).captures.first + File.open(identity_file).read.match(/(age1.+)$/).captures.first or + raise "can't get pubkey for identity #{identity_file}" end + +Given("I have the identity {string}") do |keyfile| + @identity_path = "fixtures/keys/#{keyfile}" +end + +Given("I set MOSS_STORE to a unique temporary pathname") do + ENV["MOSS_STORE"] = Dir.mktmpdir + "/store" +end + Given("I am using a temporary password store") do + @identity_path ||= "fixtures/keys/me.key" ENV["MOSS_STORE"] = Dir.mktmpdir + "/store" - add_to_store(".recipients", recipient_for_identity("fixtures/keys/me.key")) + add_to_store(".recipients", recipient_for_identity(@identity_path)) end Given("I am using the example password store") do @@ -148,6 +159,10 @@ When("I create a moss instance with identity {string}") do |keyfile| shell "#{MOSS} init fixtures/keys/#{keyfile}" end +When("I interactively create a moss instance with identity {string} and passphrase {string}") do |keyfile, passphrase| + shell "expect moss-init-with-passphrase.expect fixtures/keys/#{keyfile} #{passphrase.inspect}" +end + Then("the instance store exists") do expect(Pathname.new(ENV['MOSS_STORE'])).to be_directory end @@ -156,11 +171,15 @@ Then("the instance identity is {string}") do |keyfile| expect(store_path("../identity").read).to eq File.read("fixtures/keys/#{keyfile}") end -Then("the store root contains .recipients for the identity {string}") do |keyfile| +Then("the store root has .recipients for the identity {string}") do |keyfile| expected = `age-keygen -y fixtures/keys/#{keyfile.inspect}` expect(store_path(".recipients").read).to eq expected end +Then("the store root has recipient {string}") do |recipient| + expect(store_path(".recipients").read).to match recipient +end + Then("I can run git {string}") do |command| @i_see = shell "#{MOSS} git #{command}" end diff --git a/fixtures/keys/encrypted.key b/fixtures/keys/encrypted.key Binary files differ. diff --git a/moss-init-with-passphrase.expect b/moss-init-with-passphrase.expect @@ -0,0 +1,9 @@ +set keyfile [lindex $argv 0] +set phrase [lindex $argv 1] +spawn ./moss.rb init ${keyfile} +expect passphrase +send "${phrase}\r" +interact +catch wait result +set STATUS [ lindex $result 3 ] +exit $STATUS diff --git a/moss.rb b/moss.rb @@ -30,14 +30,34 @@ class Moss # named. "initialize" would be a good name if it weren't special to # Ruby + def encrypted?(filename) + magic = "age-encryption.org/v1" + File.read(filename, magic.length) == magic + end + + def pubkey_for_identity(filename) + pubkey = + if encrypted?(filename) + IO.popen("set -o pipefail; #{AGE} -d #{filename} | #{AGE_KEYGEN} -y -") {|f| + f.read + } + else + IO.popen("#{AGE_KEYGEN} -y #{filename}") { |f| f.read } + end + if $?.exitstatus.zero? + pubkey + else + raise "can't get public key from identity #{filename}" + end + end + def create(keyfile) keyfile.exist? or raise "Cannot read identity at #{keyfile}" - keyfile.read.match(/AGE-SECRET-KEY-1/) or - raise "#{keyfile} does not appear to be an age identity" + recipient = pubkey_for_identity(keyfile) store.mkpath FileUtils.cp(keyfile, store.parent.join("identity")) File.open(store.join(".recipients"), "w") do |f| - f.write `#{AGE_KEYGEN} -y #{keyfile.to_s.inspect}` + f.write recipient end end diff --git a/shell.nix b/shell.nix @@ -8,6 +8,7 @@ let gems = bundlerEnv { in mkShell { packages = [ gems gems.wrappedRuby + pkgs.expect bundix age pkgs.entr ]; }