arcanus 0.1.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.
@@ -0,0 +1,55 @@
1
+ require 'openssl'
2
+
3
+ module Arcanus
4
+ # Encapsulates operations for creating keys that encrypt/decrypt secrets.
5
+ class Key
6
+ DEFAULT_SIZE = 4096
7
+ PASSWORD_CIPHER = OpenSSL::Cipher.new('AES-256-CBC')
8
+
9
+ class << self
10
+ def generate(key_size_bits: DEFAULT_SIZE)
11
+ key = OpenSSL::PKey::RSA.new(key_size_bits)
12
+ new(key)
13
+ end
14
+
15
+ def from_file(file_path)
16
+ key = OpenSSL::PKey::RSA.new(File.read(file_path))
17
+ new(key)
18
+ rescue OpenSSL::PKey::RSAError
19
+ raise Errors::DecryptionError,
20
+ "Invalid PEM file #{file_path}"
21
+ end
22
+
23
+ def from_protected_file(file_path, password)
24
+ key = OpenSSL::PKey::RSA.new(File.read(file_path), password)
25
+ new(key)
26
+ rescue OpenSSL::PKey::RSAError
27
+ raise Errors::DecryptionError,
28
+ 'Either the password is invalid or the PEM file is invalid'
29
+ end
30
+ end
31
+
32
+ def initialize(key)
33
+ @key = key
34
+ end
35
+
36
+ def save(key_file_path:, password: nil)
37
+ pem =
38
+ if password
39
+ @key.to_pem(PASSWORD_CIPHER, password)
40
+ else
41
+ @key.to_pem
42
+ end
43
+
44
+ File.open(key_file_path, 'w') { |f| f.write(pem) }
45
+ end
46
+
47
+ def encrypt(plaintext)
48
+ @key.public_encrypt(plaintext)
49
+ end
50
+
51
+ def decrypt(ciphertext)
52
+ @key.private_decrypt(ciphertext)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ module Arcanus
2
+ # Encapsulates all communication to an output source.
3
+ class Output
4
+ # Creates a {Arcanus::Output} which displays nothing.
5
+ #
6
+ # @return [Arcanus::Output]
7
+ def self.silent
8
+ new(File.open('/dev/null', 'w'))
9
+ end
10
+
11
+ # Creates a new {Arcanus::Output} instance.
12
+ #
13
+ # @param stream [IO] the output destination stream.
14
+ def initialize(stream)
15
+ @output_stream = stream
16
+ end
17
+
18
+ # Print the specified output.
19
+ #
20
+ # @param [String] output the output to display
21
+ def print(output)
22
+ @output_stream.print(output)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,84 @@
1
+ require 'pathname'
2
+
3
+ module Arcanus
4
+ # Exposes information about the current git repository.
5
+ class Repo
6
+ # @param config [Arcanus::Configuration]
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ # Returns the absolute path to the root of the current repository the
12
+ # current working directory resides within.
13
+ #
14
+ # @return [String]
15
+ # @raise [Arcanus::Errors::InvalidGitRepoError] if the current directory
16
+ # doesn't reside within a git repository
17
+ def root
18
+ @root ||=
19
+ begin
20
+ git_dir = Pathname.new(File.expand_path('.'))
21
+ .enum_for(:ascend)
22
+ .find do |path|
23
+ (path + '.git').exist?
24
+ end
25
+
26
+ unless git_dir
27
+ raise Errors::InvalidGitRepoError, 'no .git directory found'
28
+ end
29
+
30
+ git_dir.to_s
31
+ end
32
+ end
33
+
34
+ # Returns an absolute path to the .git directory for a repo.
35
+ #
36
+ # @return [String]
37
+ def git_dir
38
+ @git_dir ||=
39
+ begin
40
+ git_dir = File.expand_path('.git', root)
41
+
42
+ # .git could also be a file that contains the location of the git directory
43
+ unless File.directory?(git_dir)
44
+ git_dir = File.read(git_dir)[/^gitdir: (.*)$/, 1]
45
+
46
+ # Resolve relative paths
47
+ unless git_dir.start_with?('/')
48
+ git_dir = File.expand_path(git_dir, repo_dir)
49
+ end
50
+ end
51
+
52
+ git_dir
53
+ end
54
+ end
55
+
56
+ def gitignore_file_path
57
+ File.join(root, '.gitignore')
58
+ end
59
+
60
+ def chest_file_path
61
+ File.join(root, CHEST_FILE_NAME)
62
+ end
63
+
64
+ def has_chest_file?
65
+ File.exist?(chest_file_path)
66
+ end
67
+
68
+ def locked_key_path
69
+ File.join(root, LOCKED_KEY_NAME)
70
+ end
71
+
72
+ def has_locked_key?
73
+ File.exist?(locked_key_path)
74
+ end
75
+
76
+ def unlocked_key_path
77
+ File.join(root, UNLOCKED_KEY_NAME)
78
+ end
79
+
80
+ def has_unlocked_key?
81
+ File.exist?(unlocked_key_path)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,53 @@
1
+ require 'childprocess'
2
+ require 'tempfile'
3
+
4
+ module Arcanus
5
+ # Manages execution of a child process, collecting the exit status and
6
+ # standard out/error output.
7
+ class Subprocess
8
+ # Encapsulates the result of a process.
9
+ #
10
+ # @attr_reader status [Integer] exit status code returned by process
11
+ # @attr_reader stdout [String] standard output stream output
12
+ # @attr_reader stderr [String] standard error stream output
13
+ Result = Struct.new(:status, :stdout, :stderr) do
14
+ def success?
15
+ status == 0
16
+ end
17
+ end
18
+
19
+ class << self
20
+ # Spawns a new process using the given array of arguments (the first
21
+ # element is the command).
22
+ #
23
+ # @param args [Array<String>]
24
+ # @return [Result]
25
+ def spawn(args)
26
+ process = ChildProcess.build(*args)
27
+
28
+ out, err = assign_output_streams(process)
29
+
30
+ process.start
31
+ process.wait
32
+
33
+ err.rewind
34
+ out.rewind
35
+
36
+ Result.new(process.exit_code, out.read, err.read)
37
+ end
38
+
39
+ private
40
+
41
+ # @param process [ChildProcess]
42
+ # @return [Array<IO>]
43
+ def assign_output_streams(process)
44
+ %w[out err].map do |stream_name|
45
+ ::Tempfile.new(stream_name).tap do |stream|
46
+ stream.sync = true
47
+ process.io.send("std#{stream_name}=", stream)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,132 @@
1
+ require 'forwardable'
2
+ require 'tty'
3
+
4
+ module Arcanus
5
+ # Manages all interaction with the user.
6
+ class UI
7
+ extend Forwardable
8
+
9
+ def_delegators :@shell, :ask, :confirm
10
+
11
+ # Creates a {UI} that mediates between the given input/output streams.
12
+ #
13
+ # @param input [Arcanus::Input]
14
+ # @param output [Arcanus::Output]
15
+ def initialize(input, output)
16
+ @input = input
17
+ @output = output
18
+ @pastel = Pastel.new
19
+ @shell = TTY::Shell.new
20
+ end
21
+
22
+ # Get user input, stripping extraneous whitespace.
23
+ #
24
+ # @return [String, nil]
25
+ def user_input
26
+ if input = @input.get
27
+ input.strip
28
+ end
29
+ rescue Interrupt
30
+ exit 130 # User cancelled
31
+ end
32
+
33
+ # Get user input without echoing (useful for passwords).
34
+ #
35
+ # Does not strip extraneous whitespace (since it could be part of password).
36
+ #
37
+ # @return [String, nil]
38
+ def secret_user_input
39
+ @input.get(noecho: true)
40
+ rescue Interrupt
41
+ exit 130 # User cancelled
42
+ end
43
+
44
+ # Print the specified output.
45
+ #
46
+ # @param output [String]
47
+ # @param newline [Boolean] whether to append a newline
48
+ def print(output, newline: true)
49
+ @output.print(output)
50
+ @output.print("\n") if newline
51
+ end
52
+
53
+ # Print output in bold face.
54
+ #
55
+ # @param args [Array]
56
+ # @param kwargs [Hash]
57
+ def bold(*args, **kwargs)
58
+ print(@pastel.bold(*args), **kwargs)
59
+ end
60
+
61
+ # Print the specified output in a color indicative of error.
62
+ #
63
+ # @param args [Array]
64
+ # @param kwargs [Hash]
65
+ def error(args, **kwargs)
66
+ print(@pastel.red(*args), **kwargs)
67
+ end
68
+
69
+ # Print the specified output in a bold face and color indicative of error.
70
+ #
71
+ # @param args [Array]
72
+ # @param kwargs [Hash]
73
+ def bold_error(*args, **kwargs)
74
+ print(@pastel.bold.red(*args), **kwargs)
75
+ end
76
+
77
+ # Print the specified output in a color indicative of success.
78
+ #
79
+ # @param args [Array]
80
+ # @param kwargs [Hash]
81
+ def success(*args, **kwargs)
82
+ print(@pastel.green(*args), **kwargs)
83
+ end
84
+
85
+ # Print the specified output in a color indicative of a warning.
86
+ #
87
+ # @param args [Array]
88
+ # @param kwargs [Hash]
89
+ def warning(*args, **kwargs)
90
+ print(@pastel.yellow(*args), **kwargs)
91
+ end
92
+
93
+ # Print the specified output in a color indicating information.
94
+ #
95
+ # @param args [Array]
96
+ # @param kwargs [Hash]
97
+ def info(*args, **kwargs)
98
+ print(@pastel.cyan(*args), **kwargs)
99
+ end
100
+
101
+ # Print a blank line.
102
+ def newline
103
+ print('')
104
+ end
105
+
106
+ # Execute a command with a spinner animation until it completes.
107
+ def spinner(*args, &block)
108
+ spinner = TTY::Spinner.new(*args)
109
+ spinner_thread = Thread.new do
110
+ loop do
111
+ sleep 0.1
112
+ spinner.spin
113
+ end
114
+ end
115
+
116
+ block.call
117
+ ensure
118
+ spinner_thread.kill
119
+ newline # Ensure next line of ouptut on separate line from spinner
120
+ end
121
+
122
+ # Prints a table.
123
+ #
124
+ # Customize the table by passing a block and operating on the table object
125
+ # passed to that block to add rows and customize its appearance.
126
+ def table(options = {}, &block)
127
+ t = TTY::Table.new(options)
128
+ block.call(t)
129
+ print(t.render(:unicode, options))
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,30 @@
1
+ module Arcanus
2
+ # A miscellaneous set of utility functions.
3
+ module Utils
4
+ module_function
5
+
6
+ # Converts a string containing underscores/hyphens/spaces into CamelCase.
7
+ #
8
+ # @param [String] string
9
+ # @return [String]
10
+ def camel_case(string)
11
+ string.split(/_|-| /)
12
+ .map { |part| part.sub(/^\w/, &:upcase) }
13
+ .join
14
+ end
15
+
16
+ # Convert string containing camel case or spaces into snake case.
17
+ #
18
+ # @see stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
19
+ #
20
+ # @param [String] string
21
+ # @return [String]
22
+ def snake_case(string)
23
+ string.gsub(/::/, '/')
24
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
25
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
26
+ .tr('-', '_')
27
+ .downcase
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ # Defines the gem version.
2
+ module Arcanus
3
+ VERSION = '0.1.0'
4
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arcanus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shane da Silva
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: childprocess
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.5.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.5.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: tty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.2.0
41
+ description: Tool for working with encrypted secrets in repositories
42
+ email:
43
+ - shane@dasilva.io
44
+ executables:
45
+ - arcanus
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - bin/arcanus
50
+ - lib/arcanus.rb
51
+ - lib/arcanus/chest.rb
52
+ - lib/arcanus/cli.rb
53
+ - lib/arcanus/command/base.rb
54
+ - lib/arcanus/command/edit.rb
55
+ - lib/arcanus/command/export.rb
56
+ - lib/arcanus/command/help.rb
57
+ - lib/arcanus/command/setup.rb
58
+ - lib/arcanus/command/shared/ensure_key.rb
59
+ - lib/arcanus/command/show.rb
60
+ - lib/arcanus/command/unlock.rb
61
+ - lib/arcanus/command/version.rb
62
+ - lib/arcanus/configuration.rb
63
+ - lib/arcanus/constants.rb
64
+ - lib/arcanus/error_handler.rb
65
+ - lib/arcanus/errors.rb
66
+ - lib/arcanus/input.rb
67
+ - lib/arcanus/key.rb
68
+ - lib/arcanus/output.rb
69
+ - lib/arcanus/repo.rb
70
+ - lib/arcanus/subprocess.rb
71
+ - lib/arcanus/ui.rb
72
+ - lib/arcanus/utils.rb
73
+ - lib/arcanus/version.rb
74
+ homepage: https://github.com/sds/arcanus
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 2.0.0
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.4.5.1
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Arcanus command line interface
98
+ test_files: []
99
+ has_rdoc: