arcanus 0.1.0 → 0.2.0

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