arkana 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/arkana +6 -2
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/arkana/config_parser.rb +20 -0
- data/lib/arkana/encoder.rb +50 -0
- data/lib/arkana/helpers/dotenv_helper.rb +19 -0
- data/lib/arkana/helpers/string.rb +22 -0
- data/lib/arkana/helpers/swift_template_helper.rb +12 -0
- data/lib/arkana/helpers/ui.rb +33 -0
- data/lib/arkana/models/arguments.rb +32 -0
- data/lib/arkana/models/config.rb +62 -0
- data/lib/arkana/models/salt.rb +20 -0
- data/lib/arkana/models/secret.rb +33 -0
- data/lib/arkana/models/template_arguments.rb +48 -0
- data/lib/arkana/models/type.rb +16 -0
- data/lib/arkana/salt_generator.rb +18 -0
- data/lib/arkana/swift_code_generator.rb +56 -0
- data/lib/arkana/templates/arkana.podspec.erb +15 -0
- data/lib/arkana/templates/arkana.swift.erb +52 -0
- data/lib/arkana/templates/arkana_protocol.swift.erb +15 -0
- data/lib/arkana/templates/arkana_tests.swift.erb +41 -0
- data/lib/arkana/templates/interfaces.podspec.erb +15 -0
- data/lib/arkana/templates/interfaces_package.swift.erb +27 -0
- data/lib/arkana/templates/interfaces_readme.erb +3 -0
- data/lib/arkana/templates/package.swift.erb +36 -0
- data/lib/arkana/templates/readme.erb +4 -0
- data/lib/arkana/version.rb +5 -0
- data/lib/arkana.rb +50 -0
- metadata +78 -7
- data/LICENSE +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17be699093098399339947f5a8d0df3d5ff20fdb8186cfc9bd92fa1c3aa330d2
|
4
|
+
data.tar.gz: e7b919f59bf5a491bfa9d4f166753f374a94a33a5023275e5d6d35549a0d1e58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fde3754bfdb500ef9607827f14927ab0e0d678f70ee30398b238d56ba07729d27c967ab890c0b933432898cf8577e6ed3e642ed1ec442a85020c11d54f6d0b0f
|
7
|
+
data.tar.gz: 83880b13c6eeea494d7b85d5a73fe01cba9834156eb5d5d3a51d5cc9f7030a069e312eb98d3ab017861c67b818fcc9d1abe83de3bfb0850e3ddb520df4e80e07
|
data/bin/arkana
CHANGED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "arkana"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require_relative "models/config"
|
5
|
+
require_relative "helpers/ui"
|
6
|
+
|
7
|
+
# The config parser is responsible for parsing user CLI arguments, reading the config file and returning a `Config` object.
|
8
|
+
module ConfigParser
|
9
|
+
# Parses the config file defined by the user (if any), returning a Config object.
|
10
|
+
#
|
11
|
+
# @return [Config] the config parsed from the user's options.
|
12
|
+
def self.parse(arguments)
|
13
|
+
yaml = YAML.load_file(arguments.config_filepath)
|
14
|
+
config = Config.new(yaml)
|
15
|
+
config.current_flavor = arguments.flavor
|
16
|
+
config.dotenv_filepath = arguments.dotenv_filepath
|
17
|
+
UI.warn("Dotenv file was specified but couldn't be found at '#{config.dotenv_filepath}'") if config.dotenv_filepath && !File.exist?(config.dotenv_filepath)
|
18
|
+
config
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "arkana/helpers/string"
|
4
|
+
|
5
|
+
# The encoder is responsible for finding the env vars for given keys, encoding them, and creating Secrets based on the generated data.
|
6
|
+
module Encoder
|
7
|
+
# Fetches values of each key from ENV, and encodes them using the given salt.
|
8
|
+
#
|
9
|
+
# @return [Secret[]] an array of Secret objects, which contain their keys and encoded values.
|
10
|
+
def self.encode!(keys:, salt:, current_flavor:, environments:)
|
11
|
+
keys.map do |key|
|
12
|
+
secret = find_secret!(key: key, current_flavor: current_flavor)
|
13
|
+
encoded_value = encode(secret, salt.raw)
|
14
|
+
secret_type = Type.new(string_value: secret)
|
15
|
+
protocol_key = protocol_key(key: key, environments: environments)
|
16
|
+
Secret.new(key: key, protocol_key: protocol_key, encoded_value: encoded_value, type: secret_type)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Encodes the given string, using the given cipher.
|
21
|
+
def self.encode(string, cipher)
|
22
|
+
bytes = string.encode("utf-8")
|
23
|
+
result = []
|
24
|
+
(0...bytes.length).each do |index|
|
25
|
+
byte = bytes[index].ord # Convert to its codepoint representation
|
26
|
+
result << (byte ^ cipher[index % cipher.length]) # XOR operation with a value of the cipher array.
|
27
|
+
end
|
28
|
+
|
29
|
+
encoded_key = []
|
30
|
+
result.each do |element|
|
31
|
+
# Warning: this might be specific to Swift implementation. When generating code for other languages, beware.
|
32
|
+
encoded_key << format("%#x", element) # Format the binary number to "0xAB" format.
|
33
|
+
end
|
34
|
+
|
35
|
+
encoded_key.join(", ")
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find_secret!(key:, current_flavor:)
|
39
|
+
flavor_key = "#{current_flavor.capitalize_first_letter}#{key}" if current_flavor
|
40
|
+
secret = ENV[flavor_key] if flavor_key
|
41
|
+
secret ||= ENV[key] || raise("Secret '#{flavor_key || key}' was declared but couldn't be found in the environment variables nor in the specified dotenv file.")
|
42
|
+
secret
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.protocol_key(key:, environments:)
|
46
|
+
environments.filter_map do |env|
|
47
|
+
key.delete_suffix(env) if key.end_with?(env)
|
48
|
+
end.first || key
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dotenv"
|
4
|
+
|
5
|
+
# This helper is a mere utility used to facilitate and orchestrate the loading of multiple Dotenv files.
|
6
|
+
module DotenvHelper
|
7
|
+
# Loads the appropriate dotenv file(s).
|
8
|
+
def self.load(config)
|
9
|
+
Dotenv.load(config.dotenv_filepath) if config.dotenv_filepath
|
10
|
+
# Must be loaded after loading the `config.dotenv_filepath` so they override each other in the right order
|
11
|
+
Dotenv.load(flavor_dotenv_filepath(config)) if config.current_flavor
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.flavor_dotenv_filepath(config)
|
15
|
+
dotenv_dirname = File.dirname(config.dotenv_filepath)
|
16
|
+
flavor_dotenv_filename = ".env.#{config.current_flavor.downcase}"
|
17
|
+
File.join(dotenv_dirname, flavor_dotenv_filename)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# String extensions and utilities.
|
4
|
+
class String
|
5
|
+
# Returns a string converted from an assumed PascalCase, to camelCase.
|
6
|
+
def camel_case
|
7
|
+
return self if empty?
|
8
|
+
|
9
|
+
copy = dup
|
10
|
+
copy[0] = copy[0].downcase
|
11
|
+
copy
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a string with its first character capitalized.
|
15
|
+
def capitalize_first_letter
|
16
|
+
return self if empty?
|
17
|
+
|
18
|
+
copy = dup
|
19
|
+
copy[0] = copy[0].upcase
|
20
|
+
copy
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Utilities to reduce the amount of boilerplate code in `.swift.erb` template files.
|
4
|
+
module SwiftTemplateHelper
|
5
|
+
def self.swift_type(type)
|
6
|
+
case type
|
7
|
+
when :string then "String"
|
8
|
+
when :boolean then "Bool"
|
9
|
+
else raise "Unknown variable type '#{type}' received.'"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "colorize"
|
5
|
+
|
6
|
+
# Contains utilities related to display information to the user on the Terminal (the user's interface).
|
7
|
+
module UI
|
8
|
+
# Logs the message in red color and raise an error with the given message.
|
9
|
+
def self.crash(message)
|
10
|
+
logger.fatal(message.red)
|
11
|
+
raise message
|
12
|
+
end
|
13
|
+
|
14
|
+
# Logs the message in cyan color.
|
15
|
+
def self.debug(message)
|
16
|
+
logger.debug(message.cyan)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Logs the message in green color.
|
20
|
+
def self.success(message)
|
21
|
+
logger.info(message.green)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Logs the message in yellow color.
|
25
|
+
def self.warn(message)
|
26
|
+
logger.warn(message.yellow)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Logger used to log all the messages.
|
30
|
+
def self.logger
|
31
|
+
@logger ||= Logger.new($stdout)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
# Model that parses and documents the CLI options, using `OptionParser`.
|
6
|
+
class Arguments
|
7
|
+
# @returns [string]
|
8
|
+
attr_reader :config_filepath
|
9
|
+
# @returns [string]
|
10
|
+
attr_reader :dotenv_filepath
|
11
|
+
# @returns [string]
|
12
|
+
attr_reader :flavor
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
# Default values
|
16
|
+
@config_filepath = ".arkana.yml"
|
17
|
+
@dotenv_filepath = ".env" if File.exist?(".env")
|
18
|
+
@flavor = nil
|
19
|
+
|
20
|
+
OptionParser.new do |opt|
|
21
|
+
opt.on("-c", "--config-filepath /path/to/your/.arkana.yml", "Path to your config file. Defaults to '.arkana.yml'") do |o|
|
22
|
+
@config_filepath = o
|
23
|
+
end
|
24
|
+
opt.on("-e", "--dotenv-filepath /path/to/your/.env", "Path to your dotenv file. Defaults to '.env' if one exists.") do |o|
|
25
|
+
@dotenv_filepath = o
|
26
|
+
end
|
27
|
+
opt.on("-f", "--flavor FrostedFlakes", "Flavors are useful, for instance, when generating secrets for white-label projects. See the README for more information") do |o|
|
28
|
+
@flavor = o
|
29
|
+
end
|
30
|
+
end.parse!
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Model used to hold all the configuration set up by the user (both from CLI arguments and from the config file).
|
4
|
+
class Config
|
5
|
+
# @returns [string[]]
|
6
|
+
attr_reader :environments
|
7
|
+
# @returns [string[]]
|
8
|
+
attr_reader :global_secrets
|
9
|
+
# @returns [string[]]
|
10
|
+
attr_reader :environment_secrets
|
11
|
+
# @returns [string]
|
12
|
+
attr_reader :import_name
|
13
|
+
# @returns [string]
|
14
|
+
attr_reader :namespace
|
15
|
+
# @returns [string]
|
16
|
+
attr_reader :pod_name
|
17
|
+
# @returns [string]
|
18
|
+
attr_reader :result_path
|
19
|
+
# @returns [string[]]
|
20
|
+
attr_reader :flavors
|
21
|
+
# @returns [string]
|
22
|
+
attr_reader :swift_declaration_strategy
|
23
|
+
# @returns [boolean]
|
24
|
+
attr_reader :should_generate_unit_tests
|
25
|
+
# @returns [string]
|
26
|
+
attr_reader :package_manager
|
27
|
+
|
28
|
+
# @returns [string]
|
29
|
+
attr_accessor :current_flavor
|
30
|
+
# @returns [string]
|
31
|
+
attr_accessor :dotenv_filepath
|
32
|
+
|
33
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
34
|
+
def initialize(yaml)
|
35
|
+
@environments = (yaml["environments"] || []).map(&:capitalize)
|
36
|
+
@environment_secrets = yaml["environment_secrets"] || []
|
37
|
+
@global_secrets = yaml["global_secrets"] || []
|
38
|
+
default_name = "ArkanaKeys"
|
39
|
+
@namespace = yaml["namespace"] || default_name
|
40
|
+
@import_name = yaml["import_name"] || default_name
|
41
|
+
@pod_name = yaml["pod_name"] || default_name
|
42
|
+
@result_path = yaml["result_path"] || default_name
|
43
|
+
@flavors = yaml["flavors"] || []
|
44
|
+
@swift_declaration_strategy = yaml["swift_declaration_strategy"] || "let"
|
45
|
+
@should_generate_unit_tests = yaml["should_generate_unit_tests"]
|
46
|
+
@should_generate_unit_tests = true if @should_generate_unit_tests.nil?
|
47
|
+
@package_manager = yaml["package_manager"] || "spm"
|
48
|
+
end
|
49
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
50
|
+
|
51
|
+
# TODO: Consider renaming this and environment_secrets, cuz they're confusing.
|
52
|
+
def environment_keys
|
53
|
+
result = environment_secrets.map do |k|
|
54
|
+
environments.map { |e| k + e }
|
55
|
+
end
|
56
|
+
result.flatten
|
57
|
+
end
|
58
|
+
|
59
|
+
def all_keys
|
60
|
+
global_secrets + environment_keys
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "type"
|
4
|
+
|
5
|
+
# Model used to hold the metadata for the salt bytes.
|
6
|
+
class Salt
|
7
|
+
attr_reader :raw
|
8
|
+
# Salt, formatted as a string of comma-separated hexadecimal numbers, e.g. "0x94, 0x11, 0x1b, 0x6, 0x49, 0, 0xa7"
|
9
|
+
attr_reader :formatted
|
10
|
+
|
11
|
+
def initialize(raw:)
|
12
|
+
@raw = raw
|
13
|
+
formatted_salt = []
|
14
|
+
raw.each do |element|
|
15
|
+
# Warning: this might be specific to Swift implementation. When generating code for other languages, beware.
|
16
|
+
formatted_salt << format("%#x", element)
|
17
|
+
end
|
18
|
+
@formatted = formatted_salt.join(", ")
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "type"
|
4
|
+
|
5
|
+
# Model used to hold the metadata for a secret, such as its env var key, its protocol key, its type, and encoded value.
|
6
|
+
class Secret
|
7
|
+
# @returns [string]
|
8
|
+
attr_accessor :key
|
9
|
+
# The same string as the key, except without the "environment" suffix.
|
10
|
+
# Used in the declaration of the protocol that defines the environment secrets.
|
11
|
+
# @returns [string]
|
12
|
+
attr_accessor :protocol_key
|
13
|
+
# Encoded value, formatted as a string of comma-separated hexadecimal numbers, e.g. "0x94, 0x11, 0x1b, 0x6, 0x49, 0, 0xa7"
|
14
|
+
# @returns [string]
|
15
|
+
attr_reader :encoded_value
|
16
|
+
# The type of the variable, as a language-agnostic enum.
|
17
|
+
# @returns [Type]
|
18
|
+
attr_reader :type
|
19
|
+
|
20
|
+
def initialize(key:, protocol_key:, encoded_value:, type:)
|
21
|
+
@key = key
|
22
|
+
@protocol_key = protocol_key
|
23
|
+
@encoded_value = encoded_value
|
24
|
+
@type = type
|
25
|
+
end
|
26
|
+
|
27
|
+
def environment
|
28
|
+
return :global if key == protocol_key
|
29
|
+
return key.delete_prefix(protocol_key) if key.start_with?(protocol_key)
|
30
|
+
|
31
|
+
raise("Precondition failure: the protocol_key '#{protocol_key}' is not the same as the key '#{key}' nor is its prefix. This state shouldn't be possible.")
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "arkana/models/secret"
|
4
|
+
|
5
|
+
# A class that defines template arguments in a language-agnostic form.
|
6
|
+
class TemplateArguments
|
7
|
+
# Generates template arguments for the given secrets and config specifications.
|
8
|
+
def initialize(environment_secrets:, global_secrets:, config:, salt:)
|
9
|
+
# The environments declared in the config file.
|
10
|
+
@environments = config.environments
|
11
|
+
# The salt used to encode all the secrets.
|
12
|
+
@salt = salt
|
13
|
+
# The encoded environment-specific secrets.
|
14
|
+
@environment_secrets = environment_secrets
|
15
|
+
# The encoded global secrets.
|
16
|
+
@global_secrets = global_secrets
|
17
|
+
# Name of the import statements (or the interfaces' prefixes).
|
18
|
+
@import_name = config.import_name
|
19
|
+
# Name of the pod that should be generated.
|
20
|
+
@pod_name = config.pod_name
|
21
|
+
# The top level namespace in which the keys will be generated. Often an enum.
|
22
|
+
@namespace = config.namespace
|
23
|
+
# The property declaration strategy declared in the config file.
|
24
|
+
@swift_declaration_strategy = config.swift_declaration_strategy
|
25
|
+
# Whether unit tests should be generated.
|
26
|
+
@should_generate_unit_tests = config.should_generate_unit_tests
|
27
|
+
end
|
28
|
+
|
29
|
+
def environment_protocol_secrets(environment)
|
30
|
+
@environment_secrets.select do |secret|
|
31
|
+
secret.environment == environment
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generates a new test secret for a given key, using the salt stored.
|
36
|
+
def generate_test_secret(key:)
|
37
|
+
# Yes, we encode the key as the value because this is just for testing purposes
|
38
|
+
encoded_value = Encoder.encode(key, @salt.raw)
|
39
|
+
Secret.new(key: key, protocol_key: key, encoded_value: encoded_value, type: :string)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Expose private `binding` method.
|
43
|
+
# rubocop:disable Naming/AccessorMethodName
|
44
|
+
def get_binding
|
45
|
+
binding
|
46
|
+
end
|
47
|
+
# rubocop:enable Naming/AccessorMethodName
|
48
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A namespace that defines language-agnostic variable types.
|
4
|
+
module Type
|
5
|
+
STRING = :string
|
6
|
+
BOOLEAN = :boolean
|
7
|
+
|
8
|
+
def self.new(string_value:)
|
9
|
+
case string_value
|
10
|
+
when "true", "false"
|
11
|
+
BOOLEAN
|
12
|
+
else
|
13
|
+
STRING
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require_relative "models/salt"
|
5
|
+
|
6
|
+
# Responsible for generating the salt.
|
7
|
+
module SaltGenerator
|
8
|
+
SALT_LENGTH = 64
|
9
|
+
|
10
|
+
# Generates and returns a new Salt object.
|
11
|
+
#
|
12
|
+
# @return [Salt] the salt that was just generated.
|
13
|
+
def self.generate
|
14
|
+
chars = SecureRandom.random_bytes(SALT_LENGTH).chars # Convert array of random bytes to array of characters
|
15
|
+
codepoints = chars.map(&:ord) # Convert each character to its codepoint representation
|
16
|
+
Salt.new(raw: codepoints)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
require_relative "helpers/string"
|
5
|
+
|
6
|
+
# Responsible for generating Swift source and test files.
|
7
|
+
module SwiftCodeGenerator
|
8
|
+
# Generates Swift code and test files for the given template arguments.
|
9
|
+
def self.generate(template_arguments:, config:)
|
10
|
+
interface_swift_package_dir = File.join(config.result_path, "#{config.import_name}Interfaces")
|
11
|
+
set_up_interfaces_swift_package(interface_swift_package_dir, template_arguments, config)
|
12
|
+
|
13
|
+
swift_package_dir = File.join(config.result_path, config.import_name)
|
14
|
+
set_up_swift_package(swift_package_dir, template_arguments, config)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.set_up_interfaces_swift_package(path, template_arguments, config)
|
18
|
+
dirname = File.dirname(__FILE__)
|
19
|
+
readme_template = File.read("#{dirname}/templates/interfaces_readme.erb")
|
20
|
+
package_template = File.read("#{dirname}/templates/interfaces_package.swift.erb")
|
21
|
+
podspec_template = File.read("#{dirname}/templates/interfaces.podspec.erb")
|
22
|
+
sources_dir = File.join(path, "Sources")
|
23
|
+
source_template = File.read("#{dirname}/templates/arkana_protocol.swift.erb")
|
24
|
+
FileUtils.mkdir_p(path)
|
25
|
+
FileUtils.mkdir_p(sources_dir)
|
26
|
+
render(podspec_template, template_arguments, File.join(path, "#{config.pod_name.capitalize_first_letter}Interfaces.podspec")) if config.package_manager == "cocoapods"
|
27
|
+
render(readme_template, template_arguments, File.join(path, "README.md"))
|
28
|
+
render(package_template, template_arguments, File.join(path, "Package.swift"))
|
29
|
+
render(source_template, template_arguments, File.join(sources_dir, "#{config.import_name}Interfaces.swift"))
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.set_up_swift_package(path, template_arguments, config)
|
33
|
+
dirname = File.dirname(__FILE__)
|
34
|
+
readme_template = File.read("#{dirname}/templates/readme.erb")
|
35
|
+
package_template = File.read("#{dirname}/templates/package.swift.erb")
|
36
|
+
sources_dir = File.join(path, "Sources")
|
37
|
+
source_template = File.read("#{dirname}/templates/arkana.swift.erb")
|
38
|
+
tests_dir = File.join(path, "Tests") if config.should_generate_unit_tests
|
39
|
+
tests_template = File.read("#{dirname}/templates/arkana_tests.swift.erb")
|
40
|
+
podspec_template = File.read("#{dirname}/templates/arkana.podspec.erb")
|
41
|
+
FileUtils.mkdir_p(path)
|
42
|
+
FileUtils.mkdir_p(sources_dir)
|
43
|
+
FileUtils.mkdir_p(tests_dir) if config.should_generate_unit_tests
|
44
|
+
render(readme_template, template_arguments, File.join(path, "README.md"))
|
45
|
+
render(package_template, template_arguments, File.join(path, "Package.swift"))
|
46
|
+
render(podspec_template, template_arguments, File.join(path, "#{config.pod_name.capitalize_first_letter}.podspec")) if config.package_manager == "cocoapods"
|
47
|
+
render(source_template, template_arguments, File.join(sources_dir, "#{config.import_name}.swift"))
|
48
|
+
render(tests_template, template_arguments, File.join(tests_dir, "#{config.import_name}Tests.swift")) if config.should_generate_unit_tests
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.render(template, template_arguments, destination_file)
|
52
|
+
renderer = ERB.new(template, trim_mode: ">") # Don't automatically add newlines at the end of each template tag
|
53
|
+
result = renderer.result(template_arguments.get_binding)
|
54
|
+
File.write(destination_file, result)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file was autogenerated by Arkana. Do not attempt to modify it.
|
4
|
+
|
5
|
+
Pod::Spec.new do |spec|
|
6
|
+
spec.name = '<%= @pod_name %>'
|
7
|
+
spec.summary = 'Contains env vars and secrets. Autogenerated by Arkana.'
|
8
|
+
spec.homepage = 'https://github.com/rogerluan/arkana'
|
9
|
+
spec.version = '1.0.0'
|
10
|
+
spec.authors = { 'Arkana' => 'https://github.com/rogerluan/arkana' }
|
11
|
+
spec.source = { path: "./" }
|
12
|
+
spec.source_files = 'Sources/**/*.swift'
|
13
|
+
spec.ios.deployment_target = '11.0'
|
14
|
+
spec.swift_version = '5.0'
|
15
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<% require 'arkana/helpers/string' %>
|
2
|
+
<% require 'arkana/helpers/swift_template_helper' %>
|
3
|
+
<% # TODO: Sort these import statements alphabetically %>
|
4
|
+
import Foundation
|
5
|
+
import <%= @import_name %>Interfaces
|
6
|
+
|
7
|
+
public enum <%= @namespace %> {
|
8
|
+
fileprivate static let salt: [UInt8] = [
|
9
|
+
<%= @salt.formatted %>
|
10
|
+
|
11
|
+
]
|
12
|
+
|
13
|
+
static func decode(encoded: [UInt8], cipher: [UInt8]) -> String {
|
14
|
+
return String(decoding: encoded.enumerated().map { (offset, element) in
|
15
|
+
element ^ cipher[offset % cipher.count]
|
16
|
+
}, as: UTF8.self)
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
public extension <%= @namespace %> {
|
21
|
+
struct Global: <%= @import_name %>GlobalProtocol {
|
22
|
+
public init() {}
|
23
|
+
<% for secret in @global_secrets %>
|
24
|
+
|
25
|
+
public <%= @swift_declaration_strategy %> <%= secret.key.camel_case %>: <%= SwiftTemplateHelper.swift_type(secret.type) %> = {
|
26
|
+
let encoded: [UInt8] = [
|
27
|
+
<%= secret.encoded_value %>
|
28
|
+
|
29
|
+
]
|
30
|
+
return <%= @namespace %>.decode(encoded: encoded, cipher: <%= @namespace %>.salt)
|
31
|
+
}()
|
32
|
+
<% end %>
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
<% for environment in @environments %>
|
37
|
+
public extension <%= @namespace %> {
|
38
|
+
struct <%= environment %>: <%= @import_name %>EnvironmentProtocol {
|
39
|
+
public init() {}
|
40
|
+
<% for secret in environment_protocol_secrets(environment) %>
|
41
|
+
|
42
|
+
public <%= @swift_declaration_strategy %> <%= secret.protocol_key.camel_case %>: <%= SwiftTemplateHelper.swift_type(secret.type) %> = {
|
43
|
+
let encoded: [UInt8] = [
|
44
|
+
<%= secret.encoded_value %>
|
45
|
+
|
46
|
+
]
|
47
|
+
return <%= @namespace %>.decode(encoded: encoded, cipher: <%= @namespace %>.salt)
|
48
|
+
}()
|
49
|
+
<% end %>
|
50
|
+
}
|
51
|
+
}
|
52
|
+
<% end %>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<% require 'arkana/helpers/string' %>
|
2
|
+
<% require 'arkana/helpers/swift_template_helper' %>
|
3
|
+
import Foundation
|
4
|
+
|
5
|
+
public protocol <%= @namespace %>GlobalProtocol {
|
6
|
+
<% for secret in @global_secrets %>
|
7
|
+
var <%= secret.protocol_key.camel_case %>: <%= SwiftTemplateHelper.swift_type(secret.type) %> { get }
|
8
|
+
<% end %>
|
9
|
+
}
|
10
|
+
|
11
|
+
public protocol <%= @namespace %>EnvironmentProtocol {
|
12
|
+
<% for secret in @environment_secrets.uniq(&:protocol_key) %>
|
13
|
+
var <%= secret.protocol_key.camel_case %>: <%= SwiftTemplateHelper.swift_type(secret.type) %> { get }
|
14
|
+
<% end %>
|
15
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import Foundation
|
2
|
+
import XCTest
|
3
|
+
@testable import <%= @import_name %>
|
4
|
+
|
5
|
+
|
6
|
+
final class <%= @namespace %>Tests: XCTestCase {
|
7
|
+
private var salt: [UInt8] = [
|
8
|
+
<%= @salt.formatted %>
|
9
|
+
|
10
|
+
]
|
11
|
+
|
12
|
+
func test_decodeRandomHexKey_shouldDecode() {
|
13
|
+
<% hex_key = SecureRandom.hex(64) %>
|
14
|
+
<% secret = generate_test_secret(key: hex_key) %>
|
15
|
+
let encoded: [UInt8] = [
|
16
|
+
<%= secret.encoded_value %>
|
17
|
+
|
18
|
+
]
|
19
|
+
XCTAssertEqual(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "<%= hex_key %>")
|
20
|
+
}
|
21
|
+
|
22
|
+
func test_decodeRandomBase64Key_shouldDecode() {
|
23
|
+
<% base64_key = SecureRandom.base64(64) %>
|
24
|
+
<% secret = generate_test_secret(key: base64_key) %>
|
25
|
+
let encoded: [UInt8] = [
|
26
|
+
<%= secret.encoded_value %>
|
27
|
+
|
28
|
+
]
|
29
|
+
XCTAssertEqual(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "<%= base64_key %>")
|
30
|
+
}
|
31
|
+
|
32
|
+
func test_decodeUUIDKey_shouldDecode() {
|
33
|
+
<% uuid_key = SecureRandom.uuid %>
|
34
|
+
<% secret = generate_test_secret(key: uuid_key) %>
|
35
|
+
let encoded: [UInt8] = [
|
36
|
+
<%= secret.encoded_value %>
|
37
|
+
|
38
|
+
]
|
39
|
+
XCTAssertEqual(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "<%= uuid_key %>")
|
40
|
+
}
|
41
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file was autogenerated by Arkana. Do not attempt to modify it.
|
4
|
+
|
5
|
+
Pod::Spec.new do |spec|
|
6
|
+
spec.name = '<%= @pod_name %>Interfaces'
|
7
|
+
spec.summary = 'Contains env vars and secrets. Autogenerated by Arkana.'
|
8
|
+
spec.homepage = 'https://github.com/rogerluan/arkana'
|
9
|
+
spec.version = '1.0.0'
|
10
|
+
spec.authors = { 'Arkana' => 'https://github.com/rogerluan/arkana' }
|
11
|
+
spec.source = { path: "./" }
|
12
|
+
spec.source_files = 'Sources/**/*.swift'
|
13
|
+
spec.ios.deployment_target = '11.0'
|
14
|
+
spec.swift_version = '5.0'
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
// swift-tools-version: 5.6
|
2
|
+
|
3
|
+
// This file was autogenerated by Arkana. Do not attempt to modify it.
|
4
|
+
|
5
|
+
import PackageDescription
|
6
|
+
|
7
|
+
let package = Package(
|
8
|
+
name: "<%= @import_name %>Interfaces",
|
9
|
+
platforms: [
|
10
|
+
.macOS(.v11),
|
11
|
+
.iOS(.v11),
|
12
|
+
],
|
13
|
+
products: [
|
14
|
+
.library(
|
15
|
+
name: "<%= @import_name %>Interfaces",
|
16
|
+
targets: ["<%= @import_name %>Interfaces"]
|
17
|
+
),
|
18
|
+
],
|
19
|
+
dependencies: [],
|
20
|
+
targets: [
|
21
|
+
.target(
|
22
|
+
name: "<%= @import_name %>Interfaces",
|
23
|
+
dependencies: [],
|
24
|
+
path: "Sources"
|
25
|
+
),
|
26
|
+
]
|
27
|
+
)
|
@@ -0,0 +1,3 @@
|
|
1
|
+
# <%= @namespace %>Interfaces
|
2
|
+
|
3
|
+
This Package was autogenerated by [Arkana](https://github.com/rogerluan/arkana). Do not attempt to modify it manually, otherwise your changes will be overriden in the next code generation process. Please visit [Arkana](https://github.com/rogerluan/arkana) to read more.
|
@@ -0,0 +1,36 @@
|
|
1
|
+
// swift-tools-version: 5.6
|
2
|
+
|
3
|
+
// This file was autogenerated by Arkana. Do not attempt to modify it.
|
4
|
+
|
5
|
+
import PackageDescription
|
6
|
+
|
7
|
+
let package = Package(
|
8
|
+
name: "<%= @import_name %>",
|
9
|
+
platforms: [
|
10
|
+
.macOS(.v11),
|
11
|
+
.iOS(.v11),
|
12
|
+
],
|
13
|
+
products: [
|
14
|
+
.library(
|
15
|
+
name: "<%= @import_name %>",
|
16
|
+
targets: ["<%= @import_name %>"]
|
17
|
+
),
|
18
|
+
],
|
19
|
+
dependencies: [
|
20
|
+
.package(name: "<%= @import_name%>Interfaces", path: "../<%= @import_name%>Interfaces"),
|
21
|
+
],
|
22
|
+
targets: [
|
23
|
+
.target(
|
24
|
+
name: "<%= @import_name %>",
|
25
|
+
dependencies: ["<%= @import_name %>Interfaces"],
|
26
|
+
path: "Sources"
|
27
|
+
),
|
28
|
+
<% if @should_generate_unit_tests %>
|
29
|
+
.testTarget(
|
30
|
+
name: "<%= @import_name %>Tests",
|
31
|
+
dependencies: ["<%= @import_name %>"],
|
32
|
+
path: "Tests"
|
33
|
+
),
|
34
|
+
<% end %>
|
35
|
+
]
|
36
|
+
)
|
@@ -0,0 +1,4 @@
|
|
1
|
+
# <%= @namespace %>
|
2
|
+
|
3
|
+
|
4
|
+
This Package is autogenerated by [Arkana](https://github.com/rogerluan/arkana). Do not attempt to modify it manually, otherwise your changes will be overriden on the next code generation process. Please visit [Arkana](https://github.com/rogerluan/arkana) to read more.
|
data/lib/arkana.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "arkana/config_parser"
|
4
|
+
require_relative "arkana/encoder"
|
5
|
+
require_relative "arkana/helpers/dotenv_helper"
|
6
|
+
require_relative "arkana/models/template_arguments"
|
7
|
+
require_relative "arkana/salt_generator"
|
8
|
+
require_relative "arkana/swift_code_generator"
|
9
|
+
require_relative "arkana/version"
|
10
|
+
|
11
|
+
# Top-level namespace for Arkana's execution entry point. When ran from CLI, `Arkana.run` is what is invoked.
|
12
|
+
module Arkana
|
13
|
+
def self.run(arguments)
|
14
|
+
config = ConfigParser.parse(arguments)
|
15
|
+
salt = SaltGenerator.generate
|
16
|
+
DotenvHelper.load(config)
|
17
|
+
|
18
|
+
begin
|
19
|
+
environment_secrets = Encoder.encode!(
|
20
|
+
keys: config.environment_keys,
|
21
|
+
salt: salt,
|
22
|
+
current_flavor: config.current_flavor,
|
23
|
+
environments: config.environments,
|
24
|
+
)
|
25
|
+
global_secrets = Encoder.encode!(
|
26
|
+
keys: config.global_secrets,
|
27
|
+
salt: salt,
|
28
|
+
current_flavor: config.current_flavor,
|
29
|
+
environments: config.environments,
|
30
|
+
)
|
31
|
+
rescue StandardError => e
|
32
|
+
# TODO: Improve this by creating an Env/Debug helper
|
33
|
+
puts("Something went wrong when parsing and encoding your secrets.")
|
34
|
+
puts("Current Flavor: #{config.current_flavor}")
|
35
|
+
puts("Dotenv Filepath: #{config.dotenv_filepath}")
|
36
|
+
puts("Config Filepath: #{arguments.config_filepath}")
|
37
|
+
raise e
|
38
|
+
end
|
39
|
+
template_arguments = TemplateArguments.new(
|
40
|
+
environment_secrets: environment_secrets,
|
41
|
+
global_secrets: global_secrets,
|
42
|
+
config: config,
|
43
|
+
salt: salt,
|
44
|
+
)
|
45
|
+
SwiftCodeGenerator.generate(
|
46
|
+
template_arguments: template_arguments,
|
47
|
+
config: config,
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: arkana
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roger Oba
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
12
|
-
dependencies:
|
11
|
+
date: 2022-07-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colorize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dotenv
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.7'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: yaml
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.2'
|
13
55
|
description:
|
14
56
|
email:
|
15
57
|
- rogerluan.oba@gmail.com
|
@@ -18,14 +60,42 @@ executables:
|
|
18
60
|
extensions: []
|
19
61
|
extra_rdoc_files: []
|
20
62
|
files:
|
21
|
-
- LICENSE
|
22
63
|
- bin/arkana
|
64
|
+
- bin/console
|
65
|
+
- bin/setup
|
66
|
+
- lib/arkana.rb
|
67
|
+
- lib/arkana/config_parser.rb
|
68
|
+
- lib/arkana/encoder.rb
|
69
|
+
- lib/arkana/helpers/dotenv_helper.rb
|
70
|
+
- lib/arkana/helpers/string.rb
|
71
|
+
- lib/arkana/helpers/swift_template_helper.rb
|
72
|
+
- lib/arkana/helpers/ui.rb
|
73
|
+
- lib/arkana/models/arguments.rb
|
74
|
+
- lib/arkana/models/config.rb
|
75
|
+
- lib/arkana/models/salt.rb
|
76
|
+
- lib/arkana/models/secret.rb
|
77
|
+
- lib/arkana/models/template_arguments.rb
|
78
|
+
- lib/arkana/models/type.rb
|
79
|
+
- lib/arkana/salt_generator.rb
|
80
|
+
- lib/arkana/swift_code_generator.rb
|
81
|
+
- lib/arkana/templates/arkana.podspec.erb
|
82
|
+
- lib/arkana/templates/arkana.swift.erb
|
83
|
+
- lib/arkana/templates/arkana_protocol.swift.erb
|
84
|
+
- lib/arkana/templates/arkana_tests.swift.erb
|
85
|
+
- lib/arkana/templates/interfaces.podspec.erb
|
86
|
+
- lib/arkana/templates/interfaces_package.swift.erb
|
87
|
+
- lib/arkana/templates/interfaces_readme.erb
|
88
|
+
- lib/arkana/templates/package.swift.erb
|
89
|
+
- lib/arkana/templates/readme.erb
|
90
|
+
- lib/arkana/version.rb
|
23
91
|
homepage: https://github.com/rogerluan/arkana
|
24
|
-
licenses:
|
92
|
+
licenses:
|
93
|
+
- BSD-2-Clause
|
25
94
|
metadata:
|
26
95
|
homepage_uri: https://github.com/rogerluan/arkana
|
27
96
|
source_code_uri: https://github.com/rogerluan/arkana
|
28
97
|
changelog_uri: https://github.com/rogerluan/arkana/blob/main/CHANGELOG.md
|
98
|
+
rubygems_mfa_required: 'true'
|
29
99
|
post_install_message:
|
30
100
|
rdoc_options: []
|
31
101
|
require_paths:
|
@@ -34,7 +104,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
104
|
requirements:
|
35
105
|
- - ">="
|
36
106
|
- !ruby/object:Gem::Version
|
37
|
-
version: 2.
|
107
|
+
version: 2.7.0
|
38
108
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
109
|
requirements:
|
40
110
|
- - ">="
|
@@ -44,5 +114,6 @@ requirements: []
|
|
44
114
|
rubygems_version: 3.1.6
|
45
115
|
signing_key:
|
46
116
|
specification_version: 4
|
47
|
-
summary: Store your
|
117
|
+
summary: Store your keys and secrets away from your source code. Designed for Android
|
118
|
+
and iOS projects.
|
48
119
|
test_files: []
|
data/LICENSE
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
BSD 2-Clause License
|
2
|
-
|
3
|
-
Copyright (c) 2022, Roger Oba
|
4
|
-
All rights reserved.
|
5
|
-
|
6
|
-
Redistribution and use in source and binary forms, with or without
|
7
|
-
modification, are permitted provided that the following conditions are met:
|
8
|
-
|
9
|
-
1. Redistributions of source code must retain the above copyright notice, this
|
10
|
-
list of conditions and the following disclaimer.
|
11
|
-
|
12
|
-
2. Redistributions in binary form must reproduce the above copyright notice,
|
13
|
-
this list of conditions and the following disclaimer in the documentation
|
14
|
-
and/or other materials provided with the distribution.
|
15
|
-
|
16
|
-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
17
|
-
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
18
|
-
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
19
|
-
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
20
|
-
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
21
|
-
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
22
|
-
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
23
|
-
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
24
|
-
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
-
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|