moss

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

steps.rb (8112B)


      1 MOSS="./moss.rb"
      2 
      3 require 'tmpdir'
      4 require 'fileutils'
      5 
      6 def shell(s, permit_failure: false)
      7   output = IO.popen(["bash", "-c", s], "r", 2 => 1) do |f| f.read end
      8   (permit_failure || $?.exitstatus.zero?) or
      9     raise "#{$?} : #{@i_see}"
     10   output
     11 end
     12 
     13 IDENTITY_FILE = "fixtures/keys/me.key"
     14 
     15 def store_path(s)
     16   Pathname.new(ENV["MOSS_HOME"]).join("store", s)
     17 end
     18 
     19 def add_to_store(path, content)
     20   fullpath = Pathname.new(store_path(path))
     21   fullpath.parent.mkpath
     22   File.open(fullpath, "wb") do |f|
     23     f.write(content)
     24   end
     25 end
     26 
     27 def recipient_for_identity(identity_file)
     28   File.open(identity_file).read.match(/(age1.+)$/).captures.first or
     29     raise "can't get pubkey for identity #{identity_file}"
     30 end
     31 
     32 Given("I set MOSS_HOME to a unique temporary pathname") do
     33   ENV["MOSS_HOME"] = Dir.mktmpdir
     34 end
     35 
     36 Given("I am using a temporary password store") do
     37   ENV["MOSS_HOME"] = Dir.mktmpdir
     38 
     39   add_to_store("../identity", File.read(IDENTITY_FILE))
     40   add_to_store(".recipients", recipient_for_identity(IDENTITY_FILE))
     41 end
     42 
     43 Given("I am using the example password store") do
     44   ENV["MOSS_HOME"] = "fixtures"
     45 end
     46 
     47 Given('there is a secret for {string}') do |name|
     48   shell "#{MOSS} generate #{name} 20"
     49 end
     50 
     51 When("I generate a secret for {string} with length {int}") do |name,  length|
     52   @stored_secret_name = name
     53   @i_see = shell "#{MOSS} generate #{name} #{length}"
     54 end
     55 
     56 When("I delete the secret for {string}") do |name|
     57   @i_see = shell "#{MOSS} rm #{name}"
     58 end
     59 
     60 Then('{string} does not exist') do |path|
     61   expect(store_path(path)).not_to exist
     62 end
     63 
     64 # yes, this step is a Given and a When with different text
     65 Given("there is a secret for {string} with content {string}") do |name, content|
     66   @stored_secret_name = name
     67   shell "echo -n #{content} | #{MOSS} insert #{name.inspect}"
     68 end
     69 
     70 When("I store a secret for {string} with content {string}") do |name, content|
     71   @stored_secret_name = name
     72   shell "echo -n #{content} | #{MOSS} insert #{name.inspect}"
     73 end
     74 
     75 When("I force store a secret for {string} with content {string}") do |name, content|
     76   @stored_secret_name = name
     77   shell "echo -n #{content} | #{MOSS} insert #{name.inspect} --force"
     78 end
     79 
     80 Then("I cannot store a secret for {string} with content {string}") do |name, content|
     81   @stored_secret_name = name
     82   shell "echo -n #{content} | #{MOSS} insert #{name.inspect}", permit_failure: true
     83   warn $?
     84 end
     85 
     86 Then("I see a {int} character string") do |len|
     87   expect(@i_see.length).to eq len
     88 end
     89 
     90 Then("I see the string {string}") do |s|
     91   expect(@i_see).to match(s)
     92 end
     93 
     94 Then("I see a QR code for {string}") do |s|
     95   expect(@i_see).to eq(File.read("#{__dir__}/../fixtures/#{s}.qransi"))
     96 end
     97 
     98 Then("{string} plaintext is {string}") do |name, expected|
     99   path_name = store_path(name)
    100   contents = File.read(path_name)
    101   expect(contents).to match /AGE ENCRYPTED FILE/
    102   plaintext = shell "age -i #{IDENTITY_FILE} -d #{path_name.to_s.inspect}"
    103   expect(plaintext).to eq expected
    104 end
    105 
    106 def decrypt(secret_name, keyfile)
    107   path_name = store_path(secret_name + '.age')
    108   contents = File.read(path_name)
    109   expect(contents).to match /AGE ENCRYPTED FILE/
    110   shell "age -i fixtures/keys/#{keyfile} -d #{path_name.to_s.inspect}"
    111 end
    112 
    113 Then("I can decrypt it with key {string} to {string}") do |keyfile, expected|
    114   expect(decrypt(@stored_secret_name, keyfile)).to eq expected
    115 end
    116 
    117 Then("I can decrypt {string} with key {string} to {string}") do |secret_name, keyfile, expected|
    118   expect(decrypt(secret_name, keyfile)).to eq expected
    119 end
    120 
    121 Then("I cannot decrypt it with key {string}") do |keyfile|
    122   path_name = store_path(@stored_secret_name + '.age')
    123   contents = File.read(path_name)
    124   expect(contents).to match /AGE ENCRYPTED FILE/
    125   # we expect age to fail, redirect stderr to avoid messing up test output
    126   shell "!  age -i fixtures/keys/#{keyfile} -d #{path_name.to_s.inspect} 2>&1"
    127 end
    128 
    129 Then("{string} plaintext matches {word}") do |name, re|
    130   path_name = store_path(name)
    131   contents = File.read(path_name)
    132   expect(contents).to match /AGE ENCRYPTED FILE/
    133   shell "age -i #{IDENTITY_FILE} -d #{path_name.to_s.inspect}"
    134   expect(@i_see).to match Regexp.new(re)
    135 end
    136 
    137 When("I view the secret {string}") do |name|
    138   @i_see = shell "#{MOSS} cat #{name}"
    139 end
    140 
    141 When("I view the secret {string} as QR code") do |name|
    142   @i_see = shell "#{MOSS} qr #{name}"
    143 end
    144 
    145 When("I search for {string}") do |term|
    146   @i_see = shell "#{MOSS} search #{term}"
    147 end
    148 
    149 When("I list the secrets") do
    150   @i_see = shell "#{MOSS} list"
    151 end
    152 
    153 When("I edit {string}") do |name|
    154   ENV["EDITOR"] = "./fixtures/fakedit.sh"
    155   @i_see = shell "#{MOSS} edit #{name}"
    156 end
    157 
    158 Then("the editor opens a temporary file containing {string}") do |expected|
    159   magic, pathname, content = @i_see.split("\0")
    160   # we test for this magic string to make sure that our fake editor was
    161   # invoked
    162   expect(magic).to eq('fakedit-magic')
    163   expect(pathname).to start_with(ENV['TMPDIR'])
    164   expect(content).to eq(expected)
    165 end
    166 
    167 Given("there are recipient files in different subtrees") do |table|
    168   table.hashes.each do |row|
    169     keys = row["identity"].split(/, */)
    170     recipients = keys.map { |k|
    171       recipient_for_identity("fixtures/keys/#{k}")
    172     }.join("\n")
    173     add_to_store("#{row['pathname']}/.recipients", recipients)
    174   end
    175 end
    176 
    177 When('I add a recipient for the identity {string}') do |keyfile|
    178   add_to_store(".recipients",
    179                store_path(".recipients").read +
    180                "\n" +
    181                recipient_for_identity("fixtures/keys/#{keyfile}"))
    182 end
    183 
    184 
    185 Given("the store is version-controlled") do
    186   shell "cd #{ENV["MOSS_HOME"]}/store && git init --initial-branch=main"
    187 end
    188 
    189 Then("the change to {string} is committed to version control") do |name|
    190   log = shell "cd #{ENV["MOSS_HOME"]}/store && git log #{name}"
    191   expect(log).to match /new secret/
    192 end
    193 
    194 Then("the removal of {string} is committed to version control") do |name|
    195   log = shell "cd #{ENV["MOSS_HOME"]}/store && git log --follow -- #{name}"
    196   expect(log).to match /removed secret/
    197 end
    198 
    199 Given("I do not specify a store") do
    200   ENV.delete('MOSS_HOME')
    201 end
    202 
    203 Then("the store directory is under XDG_DATA_HOME") do
    204   ENV["XDG_DATA_HOME"] = "/tmp/#{6.times.map { rand(36).to_s(36) }.join}"
    205   result = JSON.load(shell "#{MOSS} config")
    206   store = result["store"]
    207   expect(store).to start_with(ENV["XDG_DATA_HOME"])
    208 end
    209 
    210 When("I create a moss instance with identity {string}") do |keyfile|
    211   shell "#{MOSS} init fixtures/keys/#{keyfile}"
    212 end
    213 
    214 When("I interactively create a moss instance with identity {string} and passphrase {string}") do |keyfile, passphrase|
    215   shell "expect moss-init-with-passphrase.expect fixtures/keys/#{keyfile} #{passphrase.inspect}"
    216 end
    217 
    218 Then("the instance store exists") do
    219   expect(Pathname.new(ENV['MOSS_HOME']).join("store")).to be_directory
    220 end
    221 
    222 Then("the instance identity is {string}") do |keyfile|
    223   expect(store_path("../identity").read).to eq File.read("fixtures/keys/#{keyfile}")
    224 end
    225 
    226 Then("the store root has .recipients for the identity {string}") do |keyfile|
    227   expected = `age-keygen -y fixtures/keys/#{keyfile.inspect}`
    228   expect(store_path(".recipients").read).to eq expected
    229 end
    230 
    231 Then("the store root has recipient {string}") do |recipient|
    232   expect(store_path(".recipients").read).to match recipient
    233 end
    234 
    235 Then("I can run git {string}") do |command|
    236   @i_see = shell "#{MOSS} git #{command}"
    237 end
    238 
    239 Then("the store file {string} is readable only by me") do |store_file|
    240   pathname = store_path(store_file)
    241   mode =  File.stat(pathname).mode
    242   expect(mode & 077).to be_zero
    243 end
    244 
    245 Then("my identity file is readable only by me") do
    246   pathname= store_path("../identity")
    247   mode =  File.stat(pathname).mode
    248   expect(mode & 077).to be_zero
    249 end
    250 
    251 When("I run {string}") do |command|
    252   # don't fail if error returns, as we need to test that
    253   @i_see = IO.popen(["bash", "-c", "#{command.sub(/\Amoss/,  MOSS)} 2>&1"],
    254                     "r") do |f| f.read end
    255   @exit_status = $?.exitstatus
    256 end
    257 
    258 Then("it shows a usage message for {string}") do |command|
    259   expect(@exit_status).to be > 0
    260   expect(@i_see).to match /usage: moss #{command} \<file\>/
    261 end
    262 
    263 When('I re-encrypt the store') do
    264   @i_see = shell "#{MOSS} rebuild"
    265 end