arcanus 0.1.0

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