arcanus 0.1.0 → 0.2.0
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 +4 -4
- data/lib/arcanus.rb +36 -0
- data/lib/arcanus/chest.rb +44 -17
- data/lib/arcanus/command/edit.rb +17 -4
- data/lib/arcanus/command/setup.rb +15 -6
- data/lib/arcanus/command/show.rb +7 -1
- data/lib/arcanus/command/unlock.rb +2 -2
- data/lib/arcanus/configuration.rb +4 -4
- data/lib/arcanus/constants.rb +3 -3
- data/lib/arcanus/errors.rb +3 -0
- data/lib/arcanus/key.rb +1 -1
- data/lib/arcanus/repo.rb +8 -4
- data/lib/arcanus/utils.rb +10 -0
- data/lib/arcanus/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c203a66cb5ef34e876084ba7aabd877ee6228816
|
4
|
+
data.tar.gz: c2af0b10865835de171ecfcf8ec3897ace3b6cdb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c30600d873ed37082854aa17f149dcac96d7fb467fe9a9bb271c8a5ffe16f3dcce03f2f098c0f32761c9eecbfed02d4df500eb5263490eb6e83a059fe3948dec
|
7
|
+
data.tar.gz: bf2b007a2efc9f4263928ac96be9da2a1867dd01b962b1996742fd058db7d72c9b6a68dfb48fc2be7217fcacc32de32f22b57443e065ca4e691a69f39c657549
|
data/lib/arcanus.rb
CHANGED
@@ -11,3 +11,39 @@ require 'arcanus/key'
|
|
11
11
|
require 'arcanus/chest'
|
12
12
|
require 'arcanus/utils'
|
13
13
|
require 'arcanus/version'
|
14
|
+
|
15
|
+
# Exposes API for consumption by external Ruby applications.
|
16
|
+
module Arcanus
|
17
|
+
module_function
|
18
|
+
|
19
|
+
# Returns the Arcanus chest, providing access to encrypted secrets.
|
20
|
+
#
|
21
|
+
# @return [Arcanus::Chest]
|
22
|
+
def chest
|
23
|
+
Arcanus.load unless @chest
|
24
|
+
@chest
|
25
|
+
end
|
26
|
+
|
27
|
+
# Loads Arcanus chest and decrypts secrets.
|
28
|
+
#
|
29
|
+
# @param directory [String] repo directory
|
30
|
+
def load(directory = Dir.pwd)
|
31
|
+
@config = Configuration.load_applicable(directory)
|
32
|
+
@repo = Repo.new(@config)
|
33
|
+
|
34
|
+
unless File.directory?(@repo.arcanus_dir)
|
35
|
+
raise Errors::UsageError,
|
36
|
+
'Arcanus has not been initialized in this repository. ' \
|
37
|
+
'Run `arcanus setup`'
|
38
|
+
end
|
39
|
+
|
40
|
+
unless File.exist?(@repo.unlocked_key_path)
|
41
|
+
raise Errors::UsageError,
|
42
|
+
'Arcanus key has not been unlocked. ' \
|
43
|
+
'Run `arcanus unlock`'
|
44
|
+
end
|
45
|
+
|
46
|
+
@chest = Chest.new(key_file_path: @repo.unlocked_key_path,
|
47
|
+
chest_file_path: @repo.chest_file_path)
|
48
|
+
end
|
49
|
+
end
|
data/lib/arcanus/chest.rb
CHANGED
@@ -5,7 +5,7 @@ require 'yaml'
|
|
5
5
|
|
6
6
|
module Arcanus
|
7
7
|
# Encapsulates the collection of encrypted secrets managed by Arcanus.
|
8
|
-
class Chest
|
8
|
+
class Chest # rubocop:disable Metrics/ClassLength
|
9
9
|
SIGNATURE_SIZE_BITS = 256
|
10
10
|
|
11
11
|
def initialize(key_file_path:, chest_file_path:)
|
@@ -13,10 +13,10 @@ module Arcanus
|
|
13
13
|
@chest_file_path = chest_file_path
|
14
14
|
@original_encrypted_hash = YAML.load_file(chest_file_path).to_hash
|
15
15
|
@original_decrypted_hash = decrypt_hash(@original_encrypted_hash)
|
16
|
-
@hash = @original_decrypted_hash
|
16
|
+
@hash = Utils.deep_dup(@original_decrypted_hash)
|
17
17
|
end
|
18
18
|
|
19
|
-
# Access the
|
19
|
+
# Access the chest as if it were a hash.
|
20
20
|
#
|
21
21
|
# @param key [String]
|
22
22
|
# @return [Object]
|
@@ -24,11 +24,41 @@ module Arcanus
|
|
24
24
|
@hash[key]
|
25
25
|
end
|
26
26
|
|
27
|
+
# Fetch key from the chest as if it were a hash.
|
28
|
+
def fetch(*args)
|
29
|
+
@hash.fetch(*args)
|
30
|
+
end
|
31
|
+
|
27
32
|
# Returns the contents of the chest as a hash.
|
28
33
|
def contents
|
29
34
|
@hash
|
30
35
|
end
|
31
36
|
|
37
|
+
# Set value for the specified key path.
|
38
|
+
#
|
39
|
+
# @param key_path [String]
|
40
|
+
# @param value [Object]
|
41
|
+
def set(key_path, value)
|
42
|
+
keys = key_path.split('.')
|
43
|
+
nested_hash = keys[0..-2].inject(@hash) { |hash, key| hash[key] }
|
44
|
+
nested_hash[keys[-1]] = value
|
45
|
+
rescue NoMethodError
|
46
|
+
raise Arcanus::Errors::InvalidKeyPathError,
|
47
|
+
"Key path '#{key_path}' does not correspond to an actual key"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get value at the specified key path.
|
51
|
+
#
|
52
|
+
# @param key_path [String]
|
53
|
+
# @return [Object]
|
54
|
+
def get(key_path)
|
55
|
+
keys = key_path.split('.')
|
56
|
+
keys.inject(@hash) { |hash, key| hash[key] }
|
57
|
+
rescue NoMethodError
|
58
|
+
raise Arcanus::Errors::InvalidKeyPathError,
|
59
|
+
"Key path '#{key_path}' does not correspond to an actual key"
|
60
|
+
end
|
61
|
+
|
32
62
|
def update(new_hash)
|
33
63
|
@hash = new_hash
|
34
64
|
end
|
@@ -46,7 +76,7 @@ module Arcanus
|
|
46
76
|
|
47
77
|
private
|
48
78
|
|
49
|
-
def process_hash_changes(original_encrypted, original_decrypted, current)
|
79
|
+
def process_hash_changes(original_encrypted, original_decrypted, current) # rubocop:disable Metrics/MethodLength, Metrics/LineLength
|
50
80
|
result = {}
|
51
81
|
|
52
82
|
current.keys.each do |key|
|
@@ -55,9 +85,14 @@ module Arcanus
|
|
55
85
|
result[key] =
|
56
86
|
if original_encrypted.key?(key)
|
57
87
|
# Key still exists; check if modified.
|
58
|
-
if
|
59
|
-
|
60
|
-
|
88
|
+
if value.is_a?(Hash)
|
89
|
+
if original_encrypted[key].is_a?(Hash)
|
90
|
+
process_hash_changes(original_encrypted[key], original_decrypted[key], value)
|
91
|
+
else
|
92
|
+
# Key changed from single value to hash, so no previous has to compare against
|
93
|
+
process_hash_changes({}, {}, value)
|
94
|
+
end
|
95
|
+
elsif original_decrypted[key] != value
|
61
96
|
# Value was changed; encrypt the new value
|
62
97
|
encrypt_value(value)
|
63
98
|
else
|
@@ -74,22 +109,14 @@ module Arcanus
|
|
74
109
|
end
|
75
110
|
|
76
111
|
def decrypt_hash(hash)
|
77
|
-
|
78
|
-
|
79
|
-
hash.each do |key, value|
|
112
|
+
hash.each_with_object({}) do |(key, value), decrypted_hash|
|
80
113
|
begin
|
81
|
-
|
82
|
-
decrypted_hash[key] = decrypt_hash(value)
|
83
|
-
else
|
84
|
-
decrypted_hash[key] = decrypt_value(value)
|
85
|
-
end
|
114
|
+
decrypted_hash[key] = value.is_a?(Hash) ? decrypt_hash(value) : decrypt_value(value)
|
86
115
|
rescue Errors::DecryptionError => ex
|
87
116
|
raise Errors::DecryptionError,
|
88
117
|
"Problem decrypting value for key '#{key}': #{ex.message}"
|
89
118
|
end
|
90
119
|
end
|
91
|
-
|
92
|
-
decrypted_hash
|
93
120
|
end
|
94
121
|
|
95
122
|
def encrypt_value(value)
|
data/lib/arcanus/command/edit.rb
CHANGED
@@ -18,10 +18,15 @@ module Arcanus::Command
|
|
18
18
|
chest = Arcanus::Chest.new(key_file_path: repo.unlocked_key_path,
|
19
19
|
chest_file_path: repo.chest_file_path)
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
if arguments.size > 1
|
22
|
+
edit_single_key(chest, arguments[1], arguments[2])
|
23
|
+
else
|
24
|
+
# Edit entire chest
|
25
|
+
::Tempfile.new('arcanus-chest').tap do |file|
|
26
|
+
file.sync = true
|
27
|
+
file.write(chest.contents.to_yaml)
|
28
|
+
edit_until_done(chest, file.path)
|
29
|
+
end
|
25
30
|
end
|
26
31
|
end
|
27
32
|
|
@@ -31,6 +36,14 @@ module Arcanus::Command
|
|
31
36
|
!ENV['EDITOR'].strip.empty?
|
32
37
|
end
|
33
38
|
|
39
|
+
def edit_single_key(chest, key_path, new_value)
|
40
|
+
new_value = arguments[2]
|
41
|
+
old_value = chest.get(key_path)
|
42
|
+
chest.set(key_path, new_value)
|
43
|
+
chest.save
|
44
|
+
ui.success "Key '#{key_path}' updated from '#{old_value}' to '#{new_value}'"
|
45
|
+
end
|
46
|
+
|
34
47
|
def edit_until_done(chest, tempfile_path)
|
35
48
|
# Keep editing until there are no syntax errors or user decides to quit
|
36
49
|
loop do
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
1
3
|
module Arcanus::Command
|
2
4
|
class Setup < Base
|
3
5
|
description 'Create a chest to store encrypted secrets in the current repository'
|
@@ -9,16 +11,17 @@ module Arcanus::Command
|
|
9
11
|
ui.info "Let's generate one for you."
|
10
12
|
ui.newline
|
11
13
|
|
14
|
+
create_directory
|
12
15
|
create_key
|
13
16
|
create_chest
|
14
|
-
|
17
|
+
create_gitignore
|
15
18
|
|
16
19
|
ui.newline
|
17
20
|
ui.success 'You can safely commit the following files:'
|
18
|
-
ui.info Arcanus::
|
19
|
-
ui.info Arcanus::
|
21
|
+
ui.info Arcanus::CHEST_FILE_PATH
|
22
|
+
ui.info Arcanus::LOCKED_KEY_PATH
|
20
23
|
ui.success 'You must never commit the unlocked key file:'
|
21
|
-
ui.info Arcanus::
|
24
|
+
ui.info Arcanus::UNLOCKED_KEY_PATH
|
22
25
|
end
|
23
26
|
|
24
27
|
private
|
@@ -45,6 +48,10 @@ module Arcanus::Command
|
|
45
48
|
true
|
46
49
|
end
|
47
50
|
|
51
|
+
def create_directory
|
52
|
+
FileUtils.mkdir_p(repo.arcanus_dir)
|
53
|
+
end
|
54
|
+
|
48
55
|
def create_key
|
49
56
|
password = ask_password
|
50
57
|
|
@@ -90,8 +97,10 @@ module Arcanus::Command
|
|
90
97
|
File.open(repo.chest_file_path, 'w') { |f| f.write({}.to_yaml) }
|
91
98
|
end
|
92
99
|
|
93
|
-
def
|
94
|
-
File.open(repo.gitignore_file_path, 'a')
|
100
|
+
def create_gitignore
|
101
|
+
File.open(repo.gitignore_file_path, 'a') do |f|
|
102
|
+
f.write(File.basename(Arcanus::UNLOCKED_KEY_PATH))
|
103
|
+
end
|
95
104
|
end
|
96
105
|
end
|
97
106
|
end
|
data/lib/arcanus/command/show.rb
CHANGED
@@ -12,7 +12,13 @@ module Arcanus::Command
|
|
12
12
|
chest = Arcanus::Chest.new(key_file_path: repo.unlocked_key_path,
|
13
13
|
chest_file_path: repo.chest_file_path)
|
14
14
|
|
15
|
-
|
15
|
+
if arguments.size > 1
|
16
|
+
# Print specific key
|
17
|
+
ui.print chest.get(arguments[1])
|
18
|
+
else
|
19
|
+
# Print entire hash
|
20
|
+
output_colored_hash(chest.contents)
|
21
|
+
end
|
16
22
|
end
|
17
23
|
|
18
24
|
private
|
@@ -32,11 +32,11 @@ module Arcanus::Command
|
|
32
32
|
|
33
33
|
def unlock_key
|
34
34
|
loop do
|
35
|
-
ui.print 'Enter password:', newline: false
|
35
|
+
ui.print 'Enter password: ', newline: false
|
36
36
|
password = ui.secret_user_input
|
37
37
|
|
38
38
|
begin
|
39
|
-
key = Key.from_protected_file(repo.locked_key_path, password)
|
39
|
+
key = Arcanus::Key.from_protected_file(repo.locked_key_path, password)
|
40
40
|
key.save(key_file_path: repo.unlocked_key_path)
|
41
41
|
break # Key unlocked successfully
|
42
42
|
rescue Arcanus::Errors::DecryptionError => ex
|
@@ -8,15 +8,15 @@ module Arcanus
|
|
8
8
|
# this logic can be shared amongst the various components of the system.
|
9
9
|
class Configuration
|
10
10
|
# Name of the configuration file.
|
11
|
-
FILE_NAME = '.
|
11
|
+
FILE_NAME = 'config.yaml'
|
12
12
|
|
13
13
|
class << self
|
14
14
|
# Loads appropriate configuration file given the current working
|
15
15
|
# directory.
|
16
16
|
#
|
17
17
|
# @return [Arcanus::Configuration]
|
18
|
-
def load_applicable
|
19
|
-
current_directory = File.expand_path(
|
18
|
+
def load_applicable(working_directory = Dir.pwd)
|
19
|
+
current_directory = File.expand_path(working_directory)
|
20
20
|
config_file = applicable_config_file(current_directory)
|
21
21
|
|
22
22
|
if config_file
|
@@ -51,7 +51,7 @@ module Arcanus
|
|
51
51
|
def applicable_config_file(directory)
|
52
52
|
Pathname.new(directory)
|
53
53
|
.enum_for(:ascend)
|
54
|
-
.map { |dir| dir + FILE_NAME }
|
54
|
+
.map { |dir| dir + '.arcanus' + FILE_NAME }
|
55
55
|
.find do |config_file|
|
56
56
|
config_file if config_file.exist?
|
57
57
|
end
|
data/lib/arcanus/constants.rb
CHANGED
@@ -2,9 +2,9 @@
|
|
2
2
|
module Arcanus
|
3
3
|
EXECUTABLE_NAME = 'arcanus'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
CHEST_FILE_PATH = File.join('.arcanus', 'chest.yaml')
|
6
|
+
LOCKED_KEY_PATH = File.join('.arcanus', 'protected.key')
|
7
|
+
UNLOCKED_KEY_PATH = File.join('.arcanus', 'unprotected.key')
|
8
8
|
|
9
9
|
REPO_URL = 'https://github.com/sds/arcanus'
|
10
10
|
BUG_REPORT_URL = "#{REPO_URL}/issues"
|
data/lib/arcanus/errors.rb
CHANGED
@@ -29,4 +29,7 @@ module Arcanus::Errors
|
|
29
29
|
|
30
30
|
# Raised when run in a directory not part of a valid git repository.
|
31
31
|
class InvalidGitRepoError < UsageError; end
|
32
|
+
|
33
|
+
# Raised when a key path corresponding to a non-existent key is specified.
|
34
|
+
class InvalidKeyPathError < UsageError; end
|
32
35
|
end
|
data/lib/arcanus/key.rb
CHANGED
data/lib/arcanus/repo.rb
CHANGED
@@ -53,12 +53,16 @@ module Arcanus
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
def arcanus_dir
|
57
|
+
File.join(root, '.arcanus')
|
58
|
+
end
|
59
|
+
|
56
60
|
def gitignore_file_path
|
57
|
-
File.join(
|
61
|
+
File.join(arcanus_dir, '.gitignore')
|
58
62
|
end
|
59
63
|
|
60
64
|
def chest_file_path
|
61
|
-
File.join(root,
|
65
|
+
File.join(root, CHEST_FILE_PATH)
|
62
66
|
end
|
63
67
|
|
64
68
|
def has_chest_file?
|
@@ -66,7 +70,7 @@ module Arcanus
|
|
66
70
|
end
|
67
71
|
|
68
72
|
def locked_key_path
|
69
|
-
File.join(root,
|
73
|
+
File.join(root, LOCKED_KEY_PATH)
|
70
74
|
end
|
71
75
|
|
72
76
|
def has_locked_key?
|
@@ -74,7 +78,7 @@ module Arcanus
|
|
74
78
|
end
|
75
79
|
|
76
80
|
def unlocked_key_path
|
77
|
-
File.join(root,
|
81
|
+
File.join(root, UNLOCKED_KEY_PATH)
|
78
82
|
end
|
79
83
|
|
80
84
|
def has_unlocked_key?
|
data/lib/arcanus/utils.rb
CHANGED
@@ -26,5 +26,15 @@ module Arcanus
|
|
26
26
|
.tr('-', '_')
|
27
27
|
.downcase
|
28
28
|
end
|
29
|
+
|
30
|
+
# Returns a deep copy of the specified hash.
|
31
|
+
#
|
32
|
+
# @param hash [Hash]
|
33
|
+
# @return [Hash]
|
34
|
+
def deep_dup(hash)
|
35
|
+
hash.each_with_object({}) do |(key, value), dup|
|
36
|
+
dup[key] = value.is_a?(Hash) ? deep_dup(value) : value
|
37
|
+
end
|
38
|
+
end
|
29
39
|
end
|
30
40
|
end
|
data/lib/arcanus/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: arcanus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shane da Silva
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-12-
|
11
|
+
date: 2015-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: childprocess
|