mobile-secrets 0.0.9 → 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 +4 -4
- data/lib/mobile-secrets.rb +1 -1
- data/lib/resources/SecretsSwift.erb +33 -10
- data/lib/resources/example.yml +9 -4
- data/lib/src/secrets_handler.rb +40 -7
- data/lib/src/source_renderer.rb +3 -2
- metadata +19 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6bfbc5bc8fa622ef393019dc29b77d0067e329ed7dffb68f966ed659265dc221
|
|
4
|
+
data.tar.gz: 199b3c9c305fc0de836e589dddd36fdd694362a2cff6b2b7ac55719c6f044f7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c48011bde102a1994bd6e4766bc8aef593c33a3fc84448d69727987802272de54165cc9e7059a00c711aaf0c0274372a7a79cca7005e218d5dd3192e8ca2f44d
|
|
7
|
+
data.tar.gz: e67c2c5e894ce0a29487957b976e3f30f023939302a1074ca3cc289ae50df18a77b13d1d1ae5dc7885973a47311b3585bc3ec6b221c0368d46737c97a850c729
|
data/lib/mobile-secrets.rb
CHANGED
|
@@ -74,7 +74,7 @@ module MobileSecrets
|
|
|
74
74
|
MobileSecrets::SourceRenderer.new("swift").render_empty_template "#{file_path}/secrets.swift"
|
|
75
75
|
when "--edit"
|
|
76
76
|
return print_options if argv_1 == nil
|
|
77
|
-
exec("dotgpg edit
|
|
77
|
+
exec("dotgpg", "edit", argv_1)
|
|
78
78
|
when "--usage"
|
|
79
79
|
puts usage
|
|
80
80
|
else
|
|
@@ -3,16 +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
9
|
// swiftlint:disable all
|
|
9
|
-
|
|
10
|
+
<% if algorithm == "AES-GCM" %>@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
|
|
11
|
+
<% end %>final class Secrets: Sendable {
|
|
10
12
|
static let standard = Secrets()
|
|
11
13
|
private let bytes: [[UInt8]] = <%= secrets_array.to_s.gsub "],", "],\n "%>
|
|
12
14
|
<% if should_decrypt_files %>
|
|
13
15
|
private let fileNames: [[UInt8]] = <%= file_names_array.to_s.gsub "],", "],\n "%>
|
|
14
16
|
<% end %>
|
|
15
|
-
|
|
16
17
|
private init() {}
|
|
17
18
|
|
|
18
19
|
func string(forKey key: String, password: String? = nil) -> String? {
|
|
@@ -24,6 +25,27 @@ class Secrets {
|
|
|
24
25
|
return String(data: Data(value), encoding: .utf8)
|
|
25
26
|
}
|
|
26
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.
|
|
27
49
|
private func decrypt(_ input: [UInt8], password: [UInt8]) -> [UInt8]? {
|
|
28
50
|
guard !password.isEmpty else { return nil }
|
|
29
51
|
var output = [UInt8]()
|
|
@@ -32,7 +54,7 @@ class Secrets {
|
|
|
32
54
|
}
|
|
33
55
|
return output
|
|
34
56
|
}
|
|
35
|
-
|
|
57
|
+
<% end %>
|
|
36
58
|
<% if should_decrypt_files %>
|
|
37
59
|
func decryptFiles(bundle: Bundle = Bundle.main, password: String? = nil) throws {
|
|
38
60
|
try fileNames.forEach({ (fileNameBytes) in
|
|
@@ -61,7 +83,7 @@ class Secrets {
|
|
|
61
83
|
outputURL.appendPathComponent(fileName)
|
|
62
84
|
|
|
63
85
|
do {
|
|
64
|
-
let aes = try
|
|
86
|
+
let aes = try AESFileCipher(keyString: pwd)
|
|
65
87
|
let decryptedString = try aes.decrypt(fileData)
|
|
66
88
|
try decryptedString.write(to: outputURL, atomically: true, encoding: .utf8)
|
|
67
89
|
} catch let e {
|
|
@@ -69,7 +91,8 @@ class Secrets {
|
|
|
69
91
|
}
|
|
70
92
|
}
|
|
71
93
|
|
|
72
|
-
|
|
94
|
+
// AES-256-CBC cipher used for decrypting .enc file resources.
|
|
95
|
+
private struct AESFileCipher {
|
|
73
96
|
enum Error: Swift.Error {
|
|
74
97
|
case invalidKeySize
|
|
75
98
|
case encryptionFailed
|
|
@@ -79,7 +102,7 @@ class Secrets {
|
|
|
79
102
|
|
|
80
103
|
private var key: Data
|
|
81
104
|
private var ivSize: Int = kCCBlockSizeAES128
|
|
82
|
-
private let options: CCOptions
|
|
105
|
+
private let options: CCOptions = CCOptions(kCCOptionPKCS7Padding)
|
|
83
106
|
|
|
84
107
|
init(keyString: String) throws {
|
|
85
108
|
guard keyString.count == kCCKeySizeAES256 else {
|
|
@@ -104,10 +127,10 @@ class Secrets {
|
|
|
104
127
|
throw Error.encryptionFailed
|
|
105
128
|
}
|
|
106
129
|
|
|
107
|
-
let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot
|
|
130
|
+
let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot decrypt operation
|
|
108
131
|
CCOperation(kCCDecrypt), // op: CCOperation
|
|
109
|
-
CCAlgorithm(kCCAlgorithmAES),
|
|
110
|
-
options, // options: CCOptions
|
|
132
|
+
CCAlgorithm(kCCAlgorithmAES), // alg: CCAlgorithm
|
|
133
|
+
options, // options: CCOptions (PKCS7 padding)
|
|
111
134
|
keyBytesBaseAddress, // key: the "password"
|
|
112
135
|
key.count, // keyLength: the "password" size
|
|
113
136
|
dataToDecryptBytesBaseAddress, // iv: Initialization Vector
|
|
@@ -135,6 +158,6 @@ class Secrets {
|
|
|
135
158
|
|
|
136
159
|
return decryptedString
|
|
137
160
|
}
|
|
138
|
-
|
|
161
|
+
}
|
|
139
162
|
<% end %>
|
|
140
163
|
}
|
data/lib/resources/example.yml
CHANGED
|
@@ -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
|
-
|
|
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: "
|
|
13
|
-
firebase: "
|
|
14
|
-
amazon: "
|
|
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
|
data/lib/src/secrets_handler.rb
CHANGED
|
@@ -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,39 +10,52 @@ require_relative '../src/source_renderer'
|
|
|
8
10
|
module MobileSecrets
|
|
9
11
|
class SecretsHandler
|
|
10
12
|
|
|
13
|
+
SUPPORTED_ALGORITHMS = %w[XOR AES-GCM].freeze
|
|
14
|
+
|
|
11
15
|
def export_secrets path, from_encrypted_file_name
|
|
12
16
|
decrypted_config = decrypt_secrets(from_encrypted_file_name)
|
|
13
|
-
file_names_bytes, secrets_bytes = process_yaml_config decrypted_config
|
|
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
|
|
17
21
|
decrypted_config
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def process_yaml_config yaml_string
|
|
21
|
-
config = YAML.
|
|
25
|
+
config = YAML.safe_load(yaml_string)["MobileSecrets"]
|
|
22
26
|
hash_key = config["hashKey"]
|
|
23
27
|
secrets_dict = config["secrets"]
|
|
24
28
|
files = config["files"]
|
|
25
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
|
+
|
|
26
37
|
secrets_bytes = should_include_password ? [hash_key.bytes] : []
|
|
27
38
|
file_names_bytes = []
|
|
28
39
|
obfuscator = MobileSecrets::Obfuscator.new hash_key
|
|
29
40
|
|
|
30
41
|
secrets_dict.each do |key, value|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
33
48
|
end
|
|
34
49
|
|
|
35
50
|
if files
|
|
36
|
-
abort("
|
|
51
|
+
abort("hashKey must be 32 characters long for files encryption.") if hash_key.length != 32
|
|
37
52
|
files.each do |f|
|
|
38
53
|
encrypt_file hash_key, f, "#{f}.enc"
|
|
39
54
|
file_names_bytes << f.bytes
|
|
40
55
|
end
|
|
41
56
|
end
|
|
42
57
|
|
|
43
|
-
return file_names_bytes, secrets_bytes
|
|
58
|
+
return file_names_bytes, secrets_bytes, algorithm
|
|
44
59
|
end
|
|
45
60
|
|
|
46
61
|
def encrypt output_file_path, string, gpg_path
|
|
@@ -60,6 +75,24 @@ module MobileSecrets
|
|
|
60
75
|
|
|
61
76
|
private
|
|
62
77
|
|
|
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
|
+
|
|
63
96
|
def decrypt_secrets encrypted_file_name
|
|
64
97
|
gpg = Dotgpg::Dir.closest encrypted_file_name
|
|
65
98
|
output = StringIO.new
|
data/lib/src/source_renderer.rb
CHANGED
|
@@ -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,8 @@ 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)
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
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
|
|
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:
|
|
@@ -46,7 +60,7 @@ homepage: https://github.com/CyrilCermak/mobile-secrets
|
|
|
46
60
|
licenses:
|
|
47
61
|
- MIT
|
|
48
62
|
metadata: {}
|
|
49
|
-
post_install_message:
|
|
63
|
+
post_install_message:
|
|
50
64
|
rdoc_options: []
|
|
51
65
|
require_paths:
|
|
52
66
|
- lib
|
|
@@ -61,8 +75,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
61
75
|
- !ruby/object:Gem::Version
|
|
62
76
|
version: '0'
|
|
63
77
|
requirements: []
|
|
64
|
-
rubygems_version: 3.
|
|
65
|
-
signing_key:
|
|
78
|
+
rubygems_version: 3.4.10
|
|
79
|
+
signing_key:
|
|
66
80
|
specification_version: 4
|
|
67
81
|
summary: mobile-secrets tool for handling your mobile secrets
|
|
68
82
|
test_files: []
|