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.
- checksums.yaml +7 -0
- data/bin/arcanus +7 -0
- data/lib/arcanus.rb +13 -0
- data/lib/arcanus/chest.rb +137 -0
- data/lib/arcanus/cli.rb +64 -0
- data/lib/arcanus/command/base.rb +93 -0
- data/lib/arcanus/command/edit.rb +66 -0
- data/lib/arcanus/command/export.rb +55 -0
- data/lib/arcanus/command/help.rb +37 -0
- data/lib/arcanus/command/setup.rb +97 -0
- data/lib/arcanus/command/shared/ensure_key.rb +14 -0
- data/lib/arcanus/command/show.rb +34 -0
- data/lib/arcanus/command/unlock.rb +48 -0
- data/lib/arcanus/command/version.rb +9 -0
- data/lib/arcanus/configuration.rb +92 -0
- data/lib/arcanus/constants.rb +11 -0
- data/lib/arcanus/error_handler.rb +50 -0
- data/lib/arcanus/errors.rb +32 -0
- data/lib/arcanus/input.rb +24 -0
- data/lib/arcanus/key.rb +55 -0
- data/lib/arcanus/output.rb +25 -0
- data/lib/arcanus/repo.rb +84 -0
- data/lib/arcanus/subprocess.rb +53 -0
- data/lib/arcanus/ui.rb +132 -0
- data/lib/arcanus/utils.rb +30 -0
- data/lib/arcanus/version.rb +4 -0
- metadata +99 -0
data/lib/arcanus/key.rb
ADDED
@@ -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
|
data/lib/arcanus/repo.rb
ADDED
@@ -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
|
data/lib/arcanus/ui.rb
ADDED
@@ -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
|
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:
|