dotgpg 0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|