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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 52e5895e0c0560d9a912260b996068dce2b1a775
4
- data.tar.gz: d80b64ee9c1774a87b472d08f2780e65971fb075
3
+ metadata.gz: c203a66cb5ef34e876084ba7aabd877ee6228816
4
+ data.tar.gz: c2af0b10865835de171ecfcf8ec3897ace3b6cdb
5
5
  SHA512:
6
- metadata.gz: df1be1a9de6798d5ca787f6a1d5d8e92e29533afbfd7a83ef05ad879a40560377f6a6bfb4d98013471250f8433df5cf9afb84a9229a041d89233d9fc9747188f
7
- data.tar.gz: 04ce87f5fe98d26de59e1ac8e4e1d15dd9e308a595bdc313d4a51fbcfad2ab22dcdf46169ed188f55f182fb9dab8edeb31ab01ef633ba41826a18f320d1a5dd5
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.dup
16
+ @hash = Utils.deep_dup(@original_decrypted_hash)
17
17
  end
18
18
 
19
- # Access the collection as if it were a hash.
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 original_encrypted[key].is_a?(Hash) && value.is_a?(Hash)
59
- process_hash_changes(original_encrypted[key], original_decrypted[key], current[key])
60
- elsif value != original_decrypted[key]
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
- decrypted_hash = {}
78
-
79
- hash.each do |key, value|
112
+ hash.each_with_object({}) do |(key, value), decrypted_hash|
80
113
  begin
81
- if value.is_a?(Hash)
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)
@@ -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
- ::Tempfile.new('arcanus-chest').tap do |file|
22
- file.sync = true
23
- file.write(chest.contents.to_yaml)
24
- edit_until_done(chest, file.path)
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
- update_gitignore
17
+ create_gitignore
15
18
 
16
19
  ui.newline
17
20
  ui.success 'You can safely commit the following files:'
18
- ui.info Arcanus::CHEST_FILE_NAME
19
- ui.info Arcanus::LOCKED_KEY_NAME
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::UNLOCKED_KEY_NAME
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 update_gitignore
94
- File.open(repo.gitignore_file_path, 'a') { |f| f.write(Arcanus::UNLOCKED_KEY_NAME) }
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
@@ -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
- output_colored_hash(chest.contents)
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 = '.arcanus.yaml'
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(Dir.pwd)
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
@@ -2,9 +2,9 @@
2
2
  module Arcanus
3
3
  EXECUTABLE_NAME = 'arcanus'
4
4
 
5
- CHEST_FILE_NAME = '.arcanus.chest'
6
- LOCKED_KEY_NAME = '.arcanus.pem'
7
- UNLOCKED_KEY_NAME = '.arcanus.key'
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"
@@ -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
@@ -25,7 +25,7 @@ module Arcanus
25
25
  new(key)
26
26
  rescue OpenSSL::PKey::RSAError
27
27
  raise Errors::DecryptionError,
28
- 'Either the password is invalid or the PEM file is invalid'
28
+ 'Either the password is invalid or the key file is corrupted'
29
29
  end
30
30
  end
31
31
 
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(root, '.gitignore')
61
+ File.join(arcanus_dir, '.gitignore')
58
62
  end
59
63
 
60
64
  def chest_file_path
61
- File.join(root, CHEST_FILE_NAME)
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, LOCKED_KEY_NAME)
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, UNLOCKED_KEY_NAME)
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
@@ -1,4 +1,4 @@
1
1
  # Defines the gem version.
2
2
  module Arcanus
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
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.1.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-27 00:00:00.000000000 Z
11
+ date: 2015-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: childprocess