dotgpg 0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +33 -0
- data/README.md +132 -0
- data/Rakefile +7 -0
- data/bin/dotgpg +5 -0
- data/dotgpg.gemspec +22 -0
- data/lib/dotgpg/cli.rb +194 -0
- data/lib/dotgpg/dir.rb +220 -0
- data/lib/dotgpg/key.rb +105 -0
- data/lib/dotgpg/template/README.md +20 -0
- data/lib/dotgpg.rb +96 -0
- data/spec/cli_spec.rb +255 -0
- data/spec/dir_spec.rb +214 -0
- data/spec/fixture/add1.key +30 -0
- data/spec/fixture/add2.key +30 -0
- data/spec/fixture/add3.key +30 -0
- data/spec/fixture/basic/.gpg/removed1@example.com +30 -0
- data/spec/fixture/basic/.gpg/removed2@example.com +30 -0
- data/spec/fixture/basic/.gpg/removed3@example.com +0 -0
- data/spec/fixture/basic/.gpg/test2@example.com +31 -0
- data/spec/fixture/basic/.gpg/test3@example.com +31 -0
- data/spec/fixture/basic/.gpg/test@example.com +31 -0
- data/spec/fixture/basic/README.md +28 -0
- data/spec/fixture/basic/a +47 -0
- data/spec/fixture/basic/b/c +47 -0
- data/spec/fixture/gnupghome/pubring.gpg +0 -0
- data/spec/fixture/gnupghome/pubring.gpg~ +0 -0
- data/spec/fixture/gnupghome/random_seed +0 -0
- data/spec/fixture/gnupghome/secring.gpg +0 -0
- data/spec/fixture/gnupghome/trustdb.gpg +0 -0
- data/spec/fixture/secret1.key +31 -0
- data/spec/helper/assertions.rb +54 -0
- data/spec/helper.rb +19 -0
- metadata +136 -0
data/lib/dotgpg/key.rb
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
class Dotgpg
|
|
2
|
+
class Key
|
|
3
|
+
|
|
4
|
+
def self.read(file)
|
|
5
|
+
GPGME::Key.import(file).imports.map do |import|
|
|
6
|
+
GPGME::Key.find(:public, import.fingerprint)
|
|
7
|
+
end.flatten.first
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.secret_key(email=nil, force_new=nil)
|
|
11
|
+
new.secret_key(email, force_new)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def secret_key(email=nil, force_new=nil)
|
|
15
|
+
existing = existing_key(email)
|
|
16
|
+
if existing && !force_new
|
|
17
|
+
existing
|
|
18
|
+
else
|
|
19
|
+
create_new_key email
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def existing_key(email=nil)
|
|
26
|
+
existing_private_keys.detect do |k|
|
|
27
|
+
email.nil? || k.email == email
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create_new_key(email=nil)
|
|
32
|
+
name = guess_name
|
|
33
|
+
email ||= guess_email
|
|
34
|
+
|
|
35
|
+
if email
|
|
36
|
+
puts "Creating a new GPG key: #{name} <#{email}>"
|
|
37
|
+
passphrase = get_passphrase
|
|
38
|
+
else
|
|
39
|
+
puts "Creating a new GPG key for #{name}"
|
|
40
|
+
email = get_email
|
|
41
|
+
passphrase = get_passphrase
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
puts "Generating large prime numbers, please wait..."
|
|
45
|
+
ctx = GPGME::Ctx.new
|
|
46
|
+
ctx.genkey(<<EOF, nil, nil)
|
|
47
|
+
<GnupgKeyParms format="internal">
|
|
48
|
+
Key-Type: RSA
|
|
49
|
+
Key-Length: 2048
|
|
50
|
+
Subkey-Type: RSA
|
|
51
|
+
Subkey-Length: 2048
|
|
52
|
+
Name-Real: #{name}
|
|
53
|
+
Name-Comment: dotgpg
|
|
54
|
+
Name-Email: #{email}
|
|
55
|
+
Expire-Date: 0
|
|
56
|
+
Passphrase: #{passphrase}
|
|
57
|
+
</GnupgKeyParms>
|
|
58
|
+
EOF
|
|
59
|
+
|
|
60
|
+
# return the most recently created key (race!)
|
|
61
|
+
GPGME::Key.find(:secret).sort_by{ |key|
|
|
62
|
+
key.primary_subkey.timestamp
|
|
63
|
+
}.last
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def guess_name
|
|
67
|
+
name = `git config user.name 2>/dev/null`.strip
|
|
68
|
+
name = `whoami`.strip if name == ""
|
|
69
|
+
name
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def guess_email
|
|
73
|
+
email = `git config user.email 2>/dev/null`.strip
|
|
74
|
+
email if email != ""
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def get_email
|
|
78
|
+
email = ""
|
|
79
|
+
until email =~ /@/
|
|
80
|
+
email = Dotgpg.read_input "Email address: "
|
|
81
|
+
end
|
|
82
|
+
email
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_passphrase
|
|
86
|
+
passphrase = confirmation = nil
|
|
87
|
+
until passphrase && passphrase == confirmation
|
|
88
|
+
times = 0
|
|
89
|
+
until passphrase && passphrase.length >= 10
|
|
90
|
+
times += 1
|
|
91
|
+
$stderr.puts "Passphrases should be secure! (>=10 chars)" if times >= 2
|
|
92
|
+
passphrase = Dotgpg.read_passphrase("Passphrase: ")
|
|
93
|
+
end
|
|
94
|
+
until confirmation && confirmation.length >= 10
|
|
95
|
+
confirmation = Dotgpg.read_passphrase("Passphrase confirmation: ")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
passphrase
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def existing_private_keys
|
|
102
|
+
GPGME::Key.find(:secret)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
This directory contains GPG-encrypted files managed by `dotgpg`.
|
|
2
|
+
|
|
3
|
+
Getting started
|
|
4
|
+
---------------
|
|
5
|
+
|
|
6
|
+
To read files in this directory, send the output of running `dotgpg key` to someone
|
|
7
|
+
who has access already. They will be able to run `dotgpg add` on your behalf.
|
|
8
|
+
|
|
9
|
+
Usage
|
|
10
|
+
-----
|
|
11
|
+
|
|
12
|
+
You can edit any file with `dotgpg edit FILE`, and read any file with `dotgpg cat FILE`.
|
|
13
|
+
|
|
14
|
+
The edit command looks at the value of `$EDITOR`, the internet will have a tutorial on
|
|
15
|
+
how to set this up with your favourite editor.
|
|
16
|
+
|
|
17
|
+
Details
|
|
18
|
+
-------
|
|
19
|
+
|
|
20
|
+
For more information, please see `dotgpg --help`, or the [README](https://github.com/ConradIrwin/dotgpg).
|
data/lib/dotgpg.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
require 'gpgme'
|
|
7
|
+
require 'thor'
|
|
8
|
+
|
|
9
|
+
require "dotgpg/key.rb"
|
|
10
|
+
require "dotgpg/dir.rb"
|
|
11
|
+
require "dotgpg/cli.rb"
|
|
12
|
+
|
|
13
|
+
class Dotgpg
|
|
14
|
+
|
|
15
|
+
class Failure < RuntimeError; end
|
|
16
|
+
class InvalidSignature < RuntimeError; end
|
|
17
|
+
|
|
18
|
+
# This method copied directly from Pry and is
|
|
19
|
+
# Copyright (c) 2013 John Mair (banisterfiend)
|
|
20
|
+
# https://github.com/pry/pry/blob/master/LICENSE
|
|
21
|
+
def self.editor
|
|
22
|
+
configured = ENV["VISUAL"] || ENV["EDITOR"] || guess_editor
|
|
23
|
+
case configured
|
|
24
|
+
when /^mate/, /^subl/
|
|
25
|
+
configured << " -w"
|
|
26
|
+
when /^[gm]vim/
|
|
27
|
+
configured << " --nofork"
|
|
28
|
+
when /^jedit/
|
|
29
|
+
configured << " -wait"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
configured
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.guess_editor
|
|
36
|
+
%w(subl sublime-text sensible-editor editor mate nano vim vi open).detect do |editor|
|
|
37
|
+
system("which #{editor} > /dev/null 2>&1")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.read_input(prompt)
|
|
42
|
+
$stderr.print prompt
|
|
43
|
+
$stderr.flush
|
|
44
|
+
$stdin.readline.strip
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.read_passphrase(prompt)
|
|
48
|
+
`stty -echo`
|
|
49
|
+
read_input prompt
|
|
50
|
+
ensure
|
|
51
|
+
$stderr.print "\n"
|
|
52
|
+
`stty echo`
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.interactive=(bool)
|
|
56
|
+
@interactive = bool
|
|
57
|
+
if interactive?
|
|
58
|
+
# get rid of stack trace on <ctrl-c>
|
|
59
|
+
trap(:INT){ exit 2 }
|
|
60
|
+
else
|
|
61
|
+
trap(:INT, "DEFAULT")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.interactive?
|
|
66
|
+
!!@interactive
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# TODO: it'd be nice not to store the passphrase in
|
|
70
|
+
# plaintext in RAM.
|
|
71
|
+
def self.passphrase=(passphrase)
|
|
72
|
+
@passphrase = passphrase
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.warn(context, error)
|
|
76
|
+
if interactive?
|
|
77
|
+
$stderr.puts "#{context}: #{error.message}"
|
|
78
|
+
else
|
|
79
|
+
puts "raising warning"
|
|
80
|
+
raise error
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.passfunc(hook, uid_hint, passphrase_info, prev_was_bad, fd)
|
|
85
|
+
if interactive? && (!@passphrase || prev_was_bad != 0)
|
|
86
|
+
uid_hint = $1 if uid_hint =~ /<(.*)>/
|
|
87
|
+
@passphrase = read_passphrase "GPG passphrase for #{uid_hint}: "
|
|
88
|
+
elsif !@passphrase
|
|
89
|
+
raise "You must set Dotgpg.password or Dotgpg.interactive"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
io = IO.for_fd(fd, 'w')
|
|
93
|
+
io.puts(@passphrase)
|
|
94
|
+
io.flush
|
|
95
|
+
end
|
|
96
|
+
end
|
data/spec/cli_spec.rb
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
require "./spec/helper"
|
|
2
|
+
|
|
3
|
+
describe Dotgpg::Cli do
|
|
4
|
+
before do
|
|
5
|
+
@dotgpg = Dotgpg::Cli.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe "init" do
|
|
9
|
+
it "should default to the current directory" do
|
|
10
|
+
$fixture.join("create-0").mkdir
|
|
11
|
+
Dir.chdir $fixture.join("create-0") do
|
|
12
|
+
@dotgpg.invoke(:init, [])
|
|
13
|
+
assert $fixture.join("create-0", ".gpg", "test@example.com").exist?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "should create a .gpg directory" do
|
|
18
|
+
refute $fixture.join("create-1", ".gpg").exist?
|
|
19
|
+
@dotgpg.invoke(:init, [($fixture + "create-1").to_s])
|
|
20
|
+
assert $fixture.join("create-1", ".gpg").exist?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "should add the user's secret key to the .gpg directory" do
|
|
24
|
+
@dotgpg.invoke(:init, [($fixture + "create-2").to_s])
|
|
25
|
+
assert_equal $fixture.join("create-2", ".gpg", "test@example.com").read, GPGME::Key.find(:secret).first.export(armor: true).to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "should add a README to the directory" do
|
|
29
|
+
@dotgpg.invoke(:init, [($fixture + "create-3").to_s])
|
|
30
|
+
assert_equal $fixture.join("create-3", ".gpg", "test@example.com").read, GPGME::Key.find(:secret).first.export(armor: true).to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "should fail if the .gpg directory already exists" do
|
|
34
|
+
FileUtils.mkdir_p $fixture + "create-4" + ".gpg"
|
|
35
|
+
assert_fails(/\.gpg already exists/) do
|
|
36
|
+
@dotgpg.invoke(:init, [($fixture + "create-4").to_s])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "can succeed if the directory itself already exists" do
|
|
41
|
+
FileUtils.mkdir_p $fixture + "create-5"
|
|
42
|
+
@dotgpg.invoke(:init, [($fixture + "create-5").to_s])
|
|
43
|
+
assert $fixture.join("create-5", ".gpg").exist?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe "key" do
|
|
48
|
+
it "should output the secret key" do
|
|
49
|
+
assert_outputs GPGME::Key.find(:secret).first.export(armor: true).to_s do
|
|
50
|
+
@dotgpg.invoke(:key)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "add" do
|
|
56
|
+
before do
|
|
57
|
+
@path = $fixture + rand.to_s.gsub(".", "")
|
|
58
|
+
@path.mkdir
|
|
59
|
+
Dir.chdir @path do
|
|
60
|
+
Dotgpg::Cli.new.invoke(:init, [])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "should add the specified key" do
|
|
65
|
+
key_path = ($fixture + "add1.key").to_s
|
|
66
|
+
Dir.chdir @path do
|
|
67
|
+
@dotgpg.invoke(:add, [key_path])
|
|
68
|
+
end
|
|
69
|
+
assert Dotgpg::Dir.new(@path).has_key? Dotgpg::Key.read(File.read(key_path))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "should abort if the current working directory is not dotgpg" do
|
|
73
|
+
key_path = ($fixture + "add1.key").to_s
|
|
74
|
+
assert_fails(/not in a dotgpg directory/) do
|
|
75
|
+
@dotgpg.invoke(:add, [key_path])
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "should abort if the key cannot be read" do
|
|
80
|
+
key_path = ($fixture + "no-add1.key").to_s
|
|
81
|
+
Dir.chdir @path do
|
|
82
|
+
assert_fails(/no-add1.key: not a valid GPG key/) do
|
|
83
|
+
@dotgpg.invoke(:add, [key_path])
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "should abort if the key already exists" do
|
|
89
|
+
key_path = ($fixture + "add1.key").to_s
|
|
90
|
+
Dir.chdir @path do
|
|
91
|
+
Dotgpg::Cli.new.invoke(:add, [key_path])
|
|
92
|
+
|
|
93
|
+
assert_fails(/add1@example.com: already exists/) do
|
|
94
|
+
@dotgpg.invoke(:add, [key_path])
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "should do nothing if the key exists and --force is specified" do
|
|
100
|
+
key_path = ($fixture + "add1.key").to_s
|
|
101
|
+
Dir.chdir @path do
|
|
102
|
+
Dotgpg::Cli.new.invoke(:add, [key_path])
|
|
103
|
+
|
|
104
|
+
@dotgpg.invoke(:add, [key_path], force: true)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe "rm" do
|
|
110
|
+
before do
|
|
111
|
+
@path = $fixture + rand.to_s.gsub(".", "")
|
|
112
|
+
@path.mkdir
|
|
113
|
+
Dir.chdir @path do
|
|
114
|
+
Dotgpg::Cli.new.invoke(:init, [])
|
|
115
|
+
Dotgpg::Cli.new.invoke(:add, [($fixture + "add1.key").to_s])
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "should remove the specified key" do
|
|
120
|
+
Dir.chdir @path do
|
|
121
|
+
@dotgpg.invoke :rm, [".gpg/add1@example.com"]
|
|
122
|
+
end
|
|
123
|
+
refute Dotgpg::Dir.new(@path).has_key? Dotgpg::Key.read(File.read($fixture + "add1.key"))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "should find the key by email" do
|
|
127
|
+
Dir.chdir @path do
|
|
128
|
+
@dotgpg.invoke :rm, ["add1@example.com"]
|
|
129
|
+
end
|
|
130
|
+
refute Dotgpg::Dir.new(@path).has_key? Dotgpg::Key.read(File.read($fixture + "add1.key"))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "should abort if the key doesn't exist" do
|
|
134
|
+
Dir.chdir @path do
|
|
135
|
+
assert_fails(/add2@example.com: not a valid GPG key/) do
|
|
136
|
+
@dotgpg.invoke :rm, ["add2@example.com"]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "should abort if the key is the user's secret key" do
|
|
142
|
+
Dir.chdir @path do
|
|
143
|
+
assert_fails(/test@example.com: refusing to remove your own secret key/) do
|
|
144
|
+
@dotgpg.invoke :rm, ["test@example.com"]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "should do nothing if they key doesn't exist and --force is specified" do
|
|
150
|
+
Dir.chdir @path do
|
|
151
|
+
@dotgpg.invoke :rm, ["add2@example.com"], force: true
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "should remove a secret key if --force is given" do
|
|
156
|
+
key = Dotgpg::Key.read(File.read(@path + ".gpg" + "test@example.com"))
|
|
157
|
+
Dir.chdir @path do
|
|
158
|
+
@dotgpg.invoke :rm, ["test@example.com"], force: true
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
refute Dotgpg::Dir.new(@path).has_key? key
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe "cat" do
|
|
166
|
+
before do
|
|
167
|
+
Dotgpg.passphrase = 'test'
|
|
168
|
+
|
|
169
|
+
@path = $fixture + rand.to_s.gsub(".", "")
|
|
170
|
+
Dotgpg::Cli.new.invoke(:init, [@path.to_s])
|
|
171
|
+
Dotgpg::Dir.new(@path).encrypt @path + "a", "Test\n"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "should cat an existing encrypted file" do
|
|
175
|
+
assert_outputs "Test\n" do
|
|
176
|
+
@dotgpg.invoke :cat, [(@path + "a").to_s]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "should warn if a file doesn't exist" do
|
|
181
|
+
assert_warns "#{@path + "b"}: No such file or directory" do
|
|
182
|
+
@dotgpg.invoke :cat, [(@path + "b").to_s]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "should cat the existing files if a mixture is specified" do
|
|
187
|
+
assert_outputs "Test\n" do
|
|
188
|
+
assert_warns "#{@path + "b"}: No such file or directory" do
|
|
189
|
+
@dotgpg.invoke :cat, [(@path + "b").to_s, (@path + "a").to_s]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "should fail if the file is not in a .gpg directory" do
|
|
195
|
+
assert_fails "not in a dotgpg directory" do
|
|
196
|
+
@dotgpg.invoke :cat, ["/tmp/b"]
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "should fail if the passphrase is wrong" do
|
|
201
|
+
Dotgpg.passphrase = 'wrong'
|
|
202
|
+
assert_fails "Bad passphrase" do
|
|
203
|
+
@dotgpg.invoke :cat, [(@path + "a").to_s]
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
describe "edit" do
|
|
209
|
+
before do
|
|
210
|
+
Dotgpg.passphrase = 'test'
|
|
211
|
+
|
|
212
|
+
@path = $fixture + rand.to_s.gsub(".", "")
|
|
213
|
+
Dotgpg::Cli.new.invoke(:init, [@path.to_s])
|
|
214
|
+
Dotgpg::Dir.new(@path).encrypt @path + "a", "Bad test\n"
|
|
215
|
+
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it "should let you edit an existing file" do
|
|
219
|
+
ENV['EDITOR'] = "sed -i '' s/Bad/Good/"
|
|
220
|
+
path = (@path + "a").to_s
|
|
221
|
+
@dotgpg.invoke(:edit, [path])
|
|
222
|
+
assert_outputs "Good test\n" do
|
|
223
|
+
Dotgpg::Dir.new(@path).decrypt path, $stdout
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "should open a non-existing file as blank" do
|
|
228
|
+
ENV['EDITOR'] = "ruby -e 'File.write(ARGV[0], %(Good test\n)) if File.read(ARGV[0]) == %()'"
|
|
229
|
+
path = (@path + "b").to_s
|
|
230
|
+
@dotgpg.invoke(:edit, [path])
|
|
231
|
+
assert_outputs "Good test\n" do
|
|
232
|
+
Dotgpg::Dir.new(@path).decrypt path, $stdout
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it "should warn if a file cannot be decrypted" do
|
|
237
|
+
File.write(@path + "d", "not encrypted...")
|
|
238
|
+
path = (@path + "d").to_s
|
|
239
|
+
assert_warns "#{@path + "d"}: No data" do
|
|
240
|
+
@dotgpg.invoke(:edit, [path])
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it "should fail if invoking the editor doesn't work" do
|
|
245
|
+
ENV['EDITOR'] = 'not-an-editor'
|
|
246
|
+
assert_fails "Problem with editor. Not saving changes" do
|
|
247
|
+
@dotgpg.invoke :edit, [(@path + "a").to_s]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it "should edit the existing files if a mixture is specified" do
|
|
252
|
+
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
data/spec/dir_spec.rb
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
require "./spec/helper"
|
|
2
|
+
|
|
3
|
+
describe Dotgpg::Dir do
|
|
4
|
+
before do
|
|
5
|
+
@dir = Dotgpg::Dir.new($basic)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe ".closest" do
|
|
9
|
+
it "should find the current directory" do
|
|
10
|
+
Dir.chdir $basic do
|
|
11
|
+
assert_equal @dir, Dotgpg::Dir.closest(".")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "should find a specified directory" do
|
|
16
|
+
assert_equal @dir, Dotgpg::Dir.closest($basic)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "should find the directory containing the given file" do
|
|
20
|
+
assert_equal @dir, Dotgpg::Dir.closest($basic + "a")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "should find the ancestor of the directory containing the given file" do
|
|
24
|
+
assert_equal @dir, Dotgpg::Dir.closest($basic + "b" + "c")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "should not find a directory that does not exist" do
|
|
28
|
+
assert_nil Dotgpg::Dir.closest("/tmp")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "should not find a directory that only some of the files are in" do
|
|
32
|
+
assert_equal nil, Dotgpg::Dir.closest($basic, "/tmp")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "should find a directory that all of the files are in" do
|
|
36
|
+
assert_equal @dir, Dotgpg::Dir.closest($basic, $basic + "a", $basic + "b" + "c")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "dotgpg?" do
|
|
41
|
+
it 'should be true in a directory managed by dotgpg' do
|
|
42
|
+
assert_equal true, @dir.dotgpg?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'should not be true in a random directory' do
|
|
46
|
+
assert_equal false, Dotgpg::Dir.new(".").dotgpg?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "should not be true in a directory that doesn't exist" do
|
|
50
|
+
assert_equal false, Dotgpg::Dir.new(rand.to_s).dotgpg?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "known_keys" do
|
|
55
|
+
before do
|
|
56
|
+
@keys = @dir.known_keys
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "should return private keys in the truststore" do
|
|
60
|
+
assert_includes @keys, GPGME::Key.find(:secret, "test@example.com").first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "should return public keys not yet in the truststore" do
|
|
64
|
+
assert_includes @keys.map(&:email), "test2@example.com"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "should return public keys in the truststore" do
|
|
68
|
+
assert_includes @keys.map(&:email), "test2@example.com"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "all_encrypted_files" do
|
|
73
|
+
before do
|
|
74
|
+
@files = @dir.all_encrypted_files
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "should find files in the top-level" do
|
|
78
|
+
assert_includes @files, $basic + "a"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "should find files in sub-directories" do
|
|
82
|
+
assert_includes @files, $basic + "b" + "c"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "should not find unencrypted files" do
|
|
86
|
+
readme = $basic + "README.md"
|
|
87
|
+
assert readme.exist?
|
|
88
|
+
refute_includes @files, $basic + "README.md"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "should not find files through symlinks" do
|
|
92
|
+
duplicate_a = $basic + "c" + "basic" + "a"
|
|
93
|
+
assert duplicate_a.exist?
|
|
94
|
+
refute_includes @files, duplicate_a
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe "decrypt" do
|
|
99
|
+
before do
|
|
100
|
+
Dotgpg.passphrase = "test"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "should be able to decrypt files for which the secret is known" do
|
|
104
|
+
s = StringIO.new
|
|
105
|
+
@dir.decrypt $basic + "a", s
|
|
106
|
+
s.rewind
|
|
107
|
+
assert_equal "Test\n", s.read
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "should warn if the file cannot be read" do
|
|
111
|
+
assert_warns "#{$basic + "404"}: No such file or directory" do
|
|
112
|
+
@dir.decrypt $basic + "404", StringIO.new
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "should warn if the file cannot be decrypted" do
|
|
117
|
+
assert_warns "#{$basic + "README.md"}: No data" do
|
|
118
|
+
@dir.decrypt $basic + "README.md", StringIO.new
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "should raise on bad passphrase" do
|
|
123
|
+
Dotgpg.passphrase = 'wrong'
|
|
124
|
+
assert_raises GPGME::Error::BadPassphrase do
|
|
125
|
+
assert_warns nil do
|
|
126
|
+
@dir.decrypt $basic + "a", StringIO.new
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe "encrypt" do
|
|
133
|
+
before do
|
|
134
|
+
Dotgpg.passphrase = 'test'
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
after do
|
|
138
|
+
FileUtils.rm_f $basic + "test-armor"
|
|
139
|
+
FileUtils.rm_f $basic + "test-recipients"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "should armor files" do
|
|
143
|
+
@dir.encrypt $basic + "test-armor", 'test'
|
|
144
|
+
assert_match(/-----BEGIN PGP MESSAGE-----/, File.read($basic + "test-armor"))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "should encrypt files to all recipients" do
|
|
148
|
+
@dir.encrypt $basic + "test-recipients", 'test'
|
|
149
|
+
|
|
150
|
+
["D1B8548C844F4881", "54907534D1B5A86B", "8490321363F14C03"].each do |keyid|
|
|
151
|
+
assert_contains_keyid keyid, File.read($basic + "test-recipients")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
describe "add_key" do
|
|
157
|
+
before do
|
|
158
|
+
Dotgpg.passphrase = 'test'
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "should create the file in the .gpg directory" do
|
|
162
|
+
add1 = Dotgpg::Key.read $fixture + "add1.key"
|
|
163
|
+
refute $basic.join(".gpg", "add1@example.com").exist?
|
|
164
|
+
@dir.add_key add1
|
|
165
|
+
assert $basic.join(".gpg", "add1@example.com").exist?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "should add the key as a recipient on all the files" do
|
|
169
|
+
add2 = Dotgpg::Key.read $fixture + "add2.key"
|
|
170
|
+
refute_contains_keyid add2.subkeys.last.keyid, File.read($basic + "a")
|
|
171
|
+
@dir.add_key add2
|
|
172
|
+
assert_contains_keyid add2.subkeys.last.keyid, File.read($basic + "a")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "should not add the key if re-encryption fails" do
|
|
176
|
+
Dotgpg.passphrase = 'wrong'
|
|
177
|
+
add3 = Dotgpg::Key.read $fixture + "add3.key"
|
|
178
|
+
assert_raises GPGME::Error::BadPassphrase do
|
|
179
|
+
@dir.add_key add3
|
|
180
|
+
end
|
|
181
|
+
refute $basic.join(".gpg", "add3@example.com").exist?
|
|
182
|
+
refute_contains_keyid add3.subkeys.last.keyid, File.read($basic + "a")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
describe "remove_key" do
|
|
187
|
+
before do
|
|
188
|
+
Dotgpg.passphrase = 'test'
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "should remove the file from the .gpg directory" do
|
|
192
|
+
removed1 = Dotgpg::Key.read $basic.join(".gpg", "removed1@example.com")
|
|
193
|
+
assert $basic.join(".gpg", "removed1@example.com").exist?
|
|
194
|
+
@dir.remove_key removed1
|
|
195
|
+
refute $basic.join(".gpg", "removed1@example.com").exist?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "should remove the key as a recipient from all the files" do
|
|
199
|
+
removed2 = Dotgpg::Key.read $basic.join(".gpg", "removed2@example.com")
|
|
200
|
+
assert_contains_keyid removed2.subkeys.last.keyid, File.read($basic + "a")
|
|
201
|
+
@dir.remove_key removed2
|
|
202
|
+
refute_contains_keyid removed2.subkeys.last.keyid, File.read($basic + "a")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "should not remove the key if re-encryption fails" do
|
|
206
|
+
Dotgpg.passphrase = 'wrong'
|
|
207
|
+
removed3 = Dotgpg::Key.read $basic.join(".gpg", "removed3@example.com")
|
|
208
|
+
assert_raises GPGME::Error::BadPassphrase do
|
|
209
|
+
@dir.add_key removed3
|
|
210
|
+
end
|
|
211
|
+
assert $basic.join(".gpg", "removed3@example.com").exist?
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|