mobile-secrets 0.0.8 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dac63b4dd181e9c103b09d3731c278027b824efd3d111df3d1bdb9ef643916c9
4
- data.tar.gz: aa9fb1acc5d381192bc6aa407e922fc3c2d07071cb881b5bcd5c8b05bb0759d6
3
+ metadata.gz: 6bfbc5bc8fa622ef393019dc29b77d0067e329ed7dffb68f966ed659265dc221
4
+ data.tar.gz: 199b3c9c305fc0de836e589dddd36fdd694362a2cff6b2b7ac55719c6f044f7d
5
5
  SHA512:
6
- metadata.gz: ca7520a8970dcf937921463be547a72a1a7a47d992fc3ee91926f213de358ced059694d1b479c54ff8eb43838a253f2d41f32ec3df9faddd92bc45d24fd80ca9
7
- data.tar.gz: 6bbbff40235f340cd8138ab9f33295b1f3dba98fee4cb835bd1ad299edda38b131b85766f1a563e74e31952a722fd32038a21449f0ef4be84002813a2de468fa
6
+ metadata.gz: c48011bde102a1994bd6e4766bc8aef593c33a3fc84448d69727987802272de54165cc9e7059a00c711aaf0c0274372a7a79cca7005e218d5dd3192e8ca2f44d
7
+ data.tar.gz: e67c2c5e894ce0a29487957b976e3f30f023939302a1074ca3cc289ae50df18a77b13d1d1ae5dc7885973a47311b3585bc3ec6b221c0368d46737c97a850c729
@@ -20,15 +20,16 @@ module MobileSecrets
20
20
 
21
21
  def options
22
22
  opt = ""
23
- opt << "--init-gpg PATH \t\tInitialize GPG in the directory.\n"
24
- opt << "--create-template \t\tCreates a template yml file to configure the MobileSecrets\n"
25
- opt << "--import SECRETS_PATH \t\tAdds MobileSecrets to GPG secrets\n"
26
- opt << "--export PATH \t\t\tCreates source file with obfuscated secrets at given PATH\n"
27
- opt << "--encrypt-file FILE PASSWORD \tEncrypt a single file with AES\n"
28
- opt << "--usage \t\t\tManual for using MobileSecrets.\n\n"
23
+ opt << "--init-gpg PATH \t\t\tInitialize GPG in the directory.\n"
24
+ opt << "--create-template \t\t\tCreates a template yml file to configure the MobileSecrets\n"
25
+ opt << "--import SECRETS_PATH \t\t\tAdds MobileSecrets to GPG secrets\n"
26
+ opt << "--export PATH opt: ENCRYPTED_FILE_PATH \tCreates source file with obfuscated secrets at given PATH\n"
27
+ opt << "--encrypt-file FILE PASSWORD \t\tEncrypt a single file with AES\n"
28
+ opt << "--empty PATH \t\t\t\tGenerates a Secrets file without any data in it\n"
29
+ opt << "--usage \t\t\t\tManual for using MobileSecrets.\n\n"
29
30
  opt << "Examples:\n"
30
31
  opt << "--import \"./MobileSecrets.yml\"\n"
31
- opt << "--export \"./Project/Src\"\n"
32
+ opt << "--export \"./Project/Src\\n"
32
33
  opt << "--init-gpg \".\""
33
34
  opt
34
35
  end
@@ -49,22 +50,31 @@ module MobileSecrets
49
50
  FileUtils.cp("#{__dir__}/../lib/resources/example.yml", "#{Dir.pwd}#{File::SEPARATOR}MobileSecrets.yml")
50
51
  when "--export"
51
52
  return print_options if argv_1 == nil
53
+ encrypted_file_path = argv_2 ||= "secrets.gpg"
52
54
 
53
55
  secrets_handler = MobileSecrets::SecretsHandler.new
54
- secrets_handler.export_secrets argv_1
56
+ secrets_handler.export_secrets argv_1, argv_2
55
57
  when "--init-gpg"
56
58
  return print_options if argv_1 == nil
57
59
 
58
60
  Dotgpg::Cli.new.init(argv_1)
59
61
  when "--import"
60
62
  return print_options if argv_1 == nil
61
-
63
+ gpg_file = argv_2 ||= "secrets.gpg"
62
64
  file = IO.read argv_1
63
- MobileSecrets::SecretsHandler.new.encrypt "./secrets.gpg", file, nil
65
+ MobileSecrets::SecretsHandler.new.encrypt gpg_file, file, nil
64
66
  when "--encrypt-file"
65
67
  file = argv_1
66
68
  password = argv_2
67
69
  MobileSecrets::SecretsHandler.new.encrypt_file password, file, "#{file}.enc"
70
+ when "--empty"
71
+ return print_options if argv_1 == nil
72
+ file_path = argv_1
73
+
74
+ MobileSecrets::SourceRenderer.new("swift").render_empty_template "#{file_path}/secrets.swift"
75
+ when "--edit"
76
+ return print_options if argv_1 == nil
77
+ exec("dotgpg", "edit", argv_1)
68
78
  when "--usage"
69
79
  puts usage
70
80
  else
@@ -3,15 +3,17 @@
3
3
  //
4
4
 
5
5
  <% if should_decrypt_files %>import CommonCrypto<% end %>
6
+ <% if algorithm == "AES-GCM" %>import CryptoKit<% end %>
6
7
  import Foundation
7
8
 
8
- class Secrets {
9
+ // swiftlint:disable all
10
+ <% if algorithm == "AES-GCM" %>@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
11
+ <% end %>final class Secrets: Sendable {
9
12
  static let standard = Secrets()
10
13
  private let bytes: [[UInt8]] = <%= secrets_array.to_s.gsub "],", "],\n "%>
11
14
  <% if should_decrypt_files %>
12
15
  private let fileNames: [[UInt8]] = <%= file_names_array.to_s.gsub "],", "],\n "%>
13
16
  <% end %>
14
-
15
17
  private init() {}
16
18
 
17
19
  func string(forKey key: String, password: String? = nil) -> String? {
@@ -23,6 +25,27 @@ class Secrets {
23
25
  return String(data: Data(value), encoding: .utf8)
24
26
  }
25
27
 
28
+ <% if algorithm == "AES-GCM" %>
29
+ // AES-256-GCM decryption. Input layout: IV(12) + AuthTag(16) + Ciphertext(N)
30
+ private func decrypt(_ input: [UInt8], password: [UInt8]) -> [UInt8]? {
31
+ guard password.count == 32 else { return nil }
32
+ let ivSize = 12
33
+ let tagSize = 16
34
+ guard input.count > ivSize + tagSize else { return nil }
35
+ do {
36
+ let key = SymmetricKey(data: Data(password))
37
+ let nonce = try AES.GCM.Nonce(data: Data(input[0..<ivSize]))
38
+ let tag = Data(input[ivSize..<(ivSize + tagSize)])
39
+ let ciphertext = Data(input[(ivSize + tagSize)...])
40
+ let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
41
+ let decrypted = try AES.GCM.open(sealedBox, using: key)
42
+ return [UInt8](decrypted)
43
+ } catch {
44
+ return nil
45
+ }
46
+ }
47
+ <% else %>
48
+ // XOR-based deobfuscation. The key cycles over the password bytes.
26
49
  private func decrypt(_ input: [UInt8], password: [UInt8]) -> [UInt8]? {
27
50
  guard !password.isEmpty else { return nil }
28
51
  var output = [UInt8]()
@@ -31,7 +54,7 @@ class Secrets {
31
54
  }
32
55
  return output
33
56
  }
34
-
57
+ <% end %>
35
58
  <% if should_decrypt_files %>
36
59
  func decryptFiles(bundle: Bundle = Bundle.main, password: String? = nil) throws {
37
60
  try fileNames.forEach({ (fileNameBytes) in
@@ -60,7 +83,7 @@ class Secrets {
60
83
  outputURL.appendPathComponent(fileName)
61
84
 
62
85
  do {
63
- let aes = try AES(keyString: pwd)
86
+ let aes = try AESFileCipher(keyString: pwd)
64
87
  let decryptedString = try aes.decrypt(fileData)
65
88
  try decryptedString.write(to: outputURL, atomically: true, encoding: .utf8)
66
89
  } catch let e {
@@ -68,7 +91,8 @@ class Secrets {
68
91
  }
69
92
  }
70
93
 
71
- struct AES {
94
+ // AES-256-CBC cipher used for decrypting .enc file resources.
95
+ private struct AESFileCipher {
72
96
  enum Error: Swift.Error {
73
97
  case invalidKeySize
74
98
  case encryptionFailed
@@ -78,7 +102,7 @@ class Secrets {
78
102
 
79
103
  private var key: Data
80
104
  private var ivSize: Int = kCCBlockSizeAES128
81
- private let options: CCOptions = CCOptions()
105
+ private let options: CCOptions = CCOptions(kCCOptionPKCS7Padding)
82
106
 
83
107
  init(keyString: String) throws {
84
108
  guard keyString.count == kCCKeySizeAES256 else {
@@ -103,10 +127,10 @@ class Secrets {
103
127
  throw Error.encryptionFailed
104
128
  }
105
129
 
106
- let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot encrypt operation
130
+ let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot decrypt operation
107
131
  CCOperation(kCCDecrypt), // op: CCOperation
108
- CCAlgorithm(kCCAlgorithmAES), // alg: CCAlgorithm
109
- options, // options: CCOptions
132
+ CCAlgorithm(kCCAlgorithmAES), // alg: CCAlgorithm
133
+ options, // options: CCOptions (PKCS7 padding)
110
134
  keyBytesBaseAddress, // key: the "password"
111
135
  key.count, // keyLength: the "password" size
112
136
  dataToDecryptBytesBaseAddress, // iv: Initialization Vector
@@ -134,6 +158,6 @@ class Secrets {
134
158
 
135
159
  return decryptedString
136
160
  }
137
- }
161
+ }
138
162
  <% end %>
139
163
  }
@@ -0,0 +1,31 @@
1
+ //
2
+ // Autogenerated file by Mobile Secrets
3
+ //
4
+
5
+ import Foundation
6
+
7
+ // swiftlint:disable all
8
+ class Secrets {
9
+ static let standard = Secrets()
10
+ private let bytes: [[UInt8]] = [[0]]
11
+
12
+ private init() {}
13
+
14
+ func string(forKey key: String, password: String? = nil) -> String? {
15
+ let pwdBytes = password == nil ? bytes[0] : password?.map({ c in c.asciiValue ?? 0 })
16
+ guard let index = bytes.firstIndex(where: { String(data: Data($0), encoding: .utf8) == key }),
17
+ let pwd = pwdBytes,
18
+ let value = decrypt(bytes[index + 1], password: pwd) else { return nil }
19
+
20
+ return String(data: Data(value), encoding: .utf8)
21
+ }
22
+
23
+ private func decrypt(_ input: [UInt8], password: [UInt8]) -> [UInt8]? {
24
+ guard !password.isEmpty else { return nil }
25
+ var output = [UInt8]()
26
+ for byte in input.enumerated() {
27
+ output.append(byte.element ^ password[byte.offset % password.count])
28
+ }
29
+ return output
30
+ }
31
+ }
@@ -1,17 +1,22 @@
1
1
  MobileSecrets:
2
2
  # hashKey: Key that will be used to hash the secret values.
3
3
  # For encrypting files the key needs to be 32 chars long as an AES standard.
4
- hashKey: "KokoBelloKokoKokoBelloKokoKokoBe"
4
+ # When using alg: "AES-GCM" the key must also be exactly 32 characters.
5
+ hashKey: "REPLACE_THIS_32_CHAR_HASH_KEY___"
5
6
  # shouldIncludePassword: By default the password is saved in the code as a series of bytes, however it can also
6
7
  # be fetched from your API, saved in keychain and passed to the Secrets for improving the security.
7
8
  shouldIncludePassword: true
8
9
  # language: Swift is currently only supported language, Kotlin is coming soon.
9
10
  language: "Swift"
11
+ # alg: The algorithm used to obfuscate secret values. Options: "XOR" (default) or "AES-GCM".
12
+ # AES-GCM provides authenticated encryption and is stronger than XOR obfuscation.
13
+ # Note: AES-GCM requires hashKey to be exactly 32 characters and iOS 13+ / macOS 10.15+.
14
+ alg: "XOR"
10
15
  # Key-value dictionary for secrets. The key is then referenced in the code to get the secret.
11
16
  secrets:
12
- googleMaps: "123123123"
13
- firebase: "asdasdasd"
14
- amazon: "asd123asd123"
17
+ googleMaps: "YOUR_GOOGLE_MAPS_KEY"
18
+ firebase: "YOUR_FIREBASE_KEY"
19
+ amazon: "YOUR_AMAZON_KEY"
15
20
  # Optional, remove files if you do not want to encrypt them
16
21
  files:
17
22
  - tmp.txt
@@ -1,5 +1,7 @@
1
1
  require "dotgpg"
2
2
  require "yaml"
3
+ require "openssl"
4
+ require "stringio"
3
5
 
4
6
  require_relative '../src/obfuscator'
5
7
  require_relative '../src/file_handler'
@@ -8,38 +10,52 @@ require_relative '../src/source_renderer'
8
10
  module MobileSecrets
9
11
  class SecretsHandler
10
12
 
11
- def export_secrets path
12
- decrypted_config = decrypt_secrets()
13
- file_names_bytes, secrets_bytes = process_yaml_config decrypted_config
13
+ SUPPORTED_ALGORITHMS = %w[XOR AES-GCM].freeze
14
+
15
+ def export_secrets path, from_encrypted_file_name
16
+ decrypted_config = decrypt_secrets(from_encrypted_file_name)
17
+ file_names_bytes, secrets_bytes, algorithm = process_yaml_config decrypted_config
14
18
 
15
19
  renderer = MobileSecrets::SourceRenderer.new "swift"
16
- renderer.render_template secrets_bytes, file_names_bytes, "#{path}/secrets.swift"
20
+ renderer.render_template secrets_bytes, file_names_bytes, "#{path}/secrets.swift", algorithm
21
+ decrypted_config
17
22
  end
18
23
 
19
24
  def process_yaml_config yaml_string
20
- config = YAML.load(yaml_string)["MobileSecrets"]
25
+ config = YAML.safe_load(yaml_string)["MobileSecrets"]
21
26
  hash_key = config["hashKey"]
22
27
  secrets_dict = config["secrets"]
23
28
  files = config["files"]
24
29
  should_include_password = config["shouldIncludePassword"]
30
+ algorithm = (config["alg"] || "XOR").upcase
31
+
32
+ abort("Unsupported algorithm '#{algorithm}'. Valid options: #{SUPPORTED_ALGORITHMS.join(', ')}.") \
33
+ unless SUPPORTED_ALGORITHMS.include?(algorithm)
34
+ abort("hashKey must be exactly 32 characters for AES-GCM encryption.") \
35
+ if algorithm == "AES-GCM" && hash_key.length != 32
36
+
25
37
  secrets_bytes = should_include_password ? [hash_key.bytes] : []
26
38
  file_names_bytes = []
27
39
  obfuscator = MobileSecrets::Obfuscator.new hash_key
28
40
 
29
41
  secrets_dict.each do |key, value|
30
- encrypted = obfuscator.obfuscate(value)
31
- secrets_bytes << key.bytes << encrypted.bytes
42
+ if algorithm == "AES-GCM"
43
+ secrets_bytes << key.bytes << encrypt_aes_gcm(value.to_s, hash_key, key)
44
+ else
45
+ encrypted = obfuscator.obfuscate(value.to_s)
46
+ secrets_bytes << key.bytes << encrypted.bytes
47
+ end
32
48
  end
33
49
 
34
50
  if files
35
- abort("Password must be 32 characters long for files encryption.") if hash_key.length != 32
51
+ abort("hashKey must be 32 characters long for files encryption.") if hash_key.length != 32
36
52
  files.each do |f|
37
53
  encrypt_file hash_key, f, "#{f}.enc"
38
54
  file_names_bytes << f.bytes
39
55
  end
40
56
  end
41
57
 
42
- return file_names_bytes, secrets_bytes
58
+ return file_names_bytes, secrets_bytes, algorithm
43
59
  end
44
60
 
45
61
  def encrypt output_file_path, string, gpg_path
@@ -51,6 +67,7 @@ module MobileSecrets
51
67
 
52
68
  def encrypt_file password, file, output_file_path
53
69
  encryptor = FileHandler.new password
70
+ abort("Configuration contains file #{file} that cannot be found! Please check your mobile-secrets configuration or add the file into directory.") unless File.exist? file
54
71
  encrypted_content = encryptor.encrypt file
55
72
 
56
73
  File.open(output_file_path, "wb") { |f| f.write encrypted_content }
@@ -58,10 +75,28 @@ module MobileSecrets
58
75
 
59
76
  private
60
77
 
61
- def decrypt_secrets
62
- gpg = Dotgpg::Dir.new "#{Dir.pwd}/"
78
+ # Encrypts a secret value with AES-256-GCM using a deterministic IV.
79
+ # The IV is derived via HMAC-SHA256(key, "secret_name:value") truncated to 12 bytes,
80
+ # so identical inputs always produce identical ciphertext while ensuring that
81
+ # changing either the value or the secret name produces a different IV — preventing
82
+ # nonce reuse, which would be catastrophic for GCM authentication.
83
+ # Returns bytes laid out as: IV(12) + AuthTag(16) + Ciphertext(N)
84
+ def encrypt_aes_gcm(value, key_string, secret_name)
85
+ iv = OpenSSL::HMAC.digest('SHA256', key_string, "#{secret_name}:#{value}")[0, 12]
86
+ cipher = OpenSSL::Cipher::AES256.new(:GCM)
87
+ cipher.encrypt
88
+ cipher.iv = iv
89
+ cipher.key = key_string
90
+ cipher.auth_data = ""
91
+ ciphertext = cipher.update(value) + cipher.final
92
+ tag = cipher.auth_tag(16)
93
+ (iv + tag + ciphertext).bytes
94
+ end
95
+
96
+ def decrypt_secrets encrypted_file_name
97
+ gpg = Dotgpg::Dir.closest encrypted_file_name
63
98
  output = StringIO.new
64
- gpg.decrypt "#{Dir.pwd}/secrets.gpg", output
99
+ gpg.decrypt "#{Dir.pwd}/#{encrypted_file_name}", output
65
100
  output.string
66
101
  end
67
102
 
@@ -8,7 +8,7 @@ module MobileSecrets
8
8
  @source_type = source_type.downcase
9
9
  end
10
10
 
11
- def render_template secrets_bytes, file_names_bytes, output_file_path
11
+ def render_template secrets_bytes, file_names_bytes, output_file_path, algorithm = "XOR"
12
12
  template = ERB.new(File.read("#{__dir__}/../resources/SecretsSwift.erb"))
13
13
 
14
14
  case @source_type
@@ -16,7 +16,19 @@ module MobileSecrets
16
16
  File.open(output_file_path, "w") do |file|
17
17
  file.puts template.result_with_hash(secrets_array: secrets_bytes,
18
18
  file_names_array: file_names_bytes,
19
- should_decrypt_files: file_names_bytes.length > 0)
19
+ should_decrypt_files: file_names_bytes.length > 0,
20
+ algorithm: algorithm)
21
+ end
22
+ end
23
+ end
24
+
25
+ def render_empty_template output_file_path
26
+ template = File.read("#{__dir__}/../resources/SecretsSwiftEmpty.erb")
27
+
28
+ case @source_type
29
+ when "swift"
30
+ File.open(output_file_path, "w") do |file|
31
+ file.puts template
20
32
  end
21
33
  end
22
34
  end
metadata CHANGED
@@ -1,12 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mobile-secrets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Cermak
8
8
  - Joerg Nestele
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
  date: 2019-09-27 00:00:00.000000000 Z
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - '='
26
26
  - !ruby/object:Gem::Version
27
27
  version: 0.7.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: minitest-reporters
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.8'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.8'
28
42
  description: Handle mobile secrets the secure way with ease
29
43
  email: cyril.cermakk@gmail.com
30
44
  executables:
@@ -36,6 +50,7 @@ files:
36
50
  - bin/mobile-secrets
37
51
  - lib/mobile-secrets.rb
38
52
  - lib/resources/SecretsSwift.erb
53
+ - lib/resources/SecretsSwiftEmpty.erb
39
54
  - lib/resources/example.yml
40
55
  - lib/src/file_handler.rb
41
56
  - lib/src/obfuscator.rb
@@ -45,7 +60,7 @@ homepage: https://github.com/CyrilCermak/mobile-secrets
45
60
  licenses:
46
61
  - MIT
47
62
  metadata: {}
48
- post_install_message:
63
+ post_install_message:
49
64
  rdoc_options: []
50
65
  require_paths:
51
66
  - lib
@@ -60,8 +75,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
60
75
  - !ruby/object:Gem::Version
61
76
  version: '0'
62
77
  requirements: []
63
- rubygems_version: 3.0.6
64
- signing_key:
78
+ rubygems_version: 3.4.10
79
+ signing_key:
65
80
  specification_version: 4
66
81
  summary: mobile-secrets tool for handling your mobile secrets
67
82
  test_files: []