mobile-secrets 0.0.4 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/mobile-secrets.rb +25 -10
- data/lib/resources/SecretsSwift.erb +140 -0
- data/lib/resources/SecretsSwiftEmpty.erb +31 -0
- data/lib/resources/example.yml +14 -3
- data/lib/src/file_handler.rb +20 -0
- data/lib/src/secrets_handler.rb +35 -20
- data/lib/src/source_renderer.rb +36 -0
- metadata +7 -6
- data/lib/resources/SecretsTemplate.swift +0 -28
- data/test/test_hola.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f4f08928281f7ec09b0f307fde69ac6a240fc006cdb398741b94b94906952f6
|
4
|
+
data.tar.gz: 2b28b5fccdd4b442bf229733eaad00e170baee9e4c5505a925761f947929bda8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f7805f87c8cf0713394b29e4f40da1654661ea736640de9fb59f95b8aff7e2928b5941a67f993af709c3b31cb62f4d1f1fbcbca88bd856303b05ddb9966ed13
|
7
|
+
data.tar.gz: 693fabe5ec237be1fcaf50ca841b60df85dccde3d017bf97e059cffe8ad6b216e8656be7ebef97688fb6e25318e93bfd36cbc09b2752fbd69ba42d39be1f72a3
|
data/lib/mobile-secrets.rb
CHANGED
@@ -20,14 +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 \tAdds MobileSecrets to GPG secrets\n"
|
26
|
-
opt << "--export PATH \
|
27
|
-
opt << "--
|
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"
|
28
30
|
opt << "Examples:\n"
|
29
31
|
opt << "--import \"./MobileSecrets.yml\"\n"
|
30
|
-
opt << "--export \"./Project/Src
|
32
|
+
opt << "--export \"./Project/Src\\n"
|
31
33
|
opt << "--init-gpg \".\""
|
32
34
|
opt
|
33
35
|
end
|
@@ -37,7 +39,7 @@ module MobileSecrets
|
|
37
39
|
usage << "1) Create gpg first with --init-gpg \".\"\n"
|
38
40
|
usage << "2) Create a template for MobileSecrets with --create-template\n"
|
39
41
|
usage << "3) Configure MobileSecrets.yml with your hash key, secrets etc\n"
|
40
|
-
usage << "4) Import edited template to encrypted secret.gpg with --import\n"
|
42
|
+
usage << "4) Import edited template to encrypted secret.gpg with --import ./MobileSecrets.yml\n"
|
41
43
|
usage << "5) Export secrets from secrets.gpg to source file with --export and PATH to project\n"
|
42
44
|
usage << "6) Add exported source file to the project\n"
|
43
45
|
end
|
@@ -48,18 +50,31 @@ module MobileSecrets
|
|
48
50
|
FileUtils.cp("#{__dir__}/../lib/resources/example.yml", "#{Dir.pwd}#{File::SEPARATOR}MobileSecrets.yml")
|
49
51
|
when "--export"
|
50
52
|
return print_options if argv_1 == nil
|
53
|
+
encrypted_file_path = argv_2 ||= "secrets.gpg"
|
51
54
|
|
52
55
|
secrets_handler = MobileSecrets::SecretsHandler.new
|
53
|
-
secrets_handler.export_secrets argv_1
|
56
|
+
secrets_handler.export_secrets argv_1, argv_2
|
54
57
|
when "--init-gpg"
|
55
58
|
return print_options if argv_1 == nil
|
56
59
|
|
57
60
|
Dotgpg::Cli.new.init(argv_1)
|
58
61
|
when "--import"
|
59
62
|
return print_options if argv_1 == nil
|
60
|
-
|
63
|
+
gpg_file = argv_2 ||= "secrets.gpg"
|
61
64
|
file = IO.read argv_1
|
62
|
-
MobileSecrets::SecretsHandler.new.encrypt
|
65
|
+
MobileSecrets::SecretsHandler.new.encrypt gpg_file, file, nil
|
66
|
+
when "--encrypt-file"
|
67
|
+
file = argv_1
|
68
|
+
password = argv_2
|
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}")
|
63
78
|
when "--usage"
|
64
79
|
puts usage
|
65
80
|
else
|
@@ -0,0 +1,140 @@
|
|
1
|
+
//
|
2
|
+
// Autogenerated file by Mobile Secrets
|
3
|
+
//
|
4
|
+
|
5
|
+
<% if should_decrypt_files %>import CommonCrypto<% end %>
|
6
|
+
import Foundation
|
7
|
+
|
8
|
+
// swiftlint:disable all
|
9
|
+
class Secrets {
|
10
|
+
static let standard = Secrets()
|
11
|
+
private let bytes: [[UInt8]] = <%= secrets_array.to_s.gsub "],", "],\n "%>
|
12
|
+
<% if should_decrypt_files %>
|
13
|
+
private let fileNames: [[UInt8]] = <%= file_names_array.to_s.gsub "],", "],\n "%>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
private init() {}
|
17
|
+
|
18
|
+
func string(forKey key: String, password: String? = nil) -> String? {
|
19
|
+
let pwdBytes = password == nil ? bytes[0] : password?.map({ c in c.asciiValue ?? 0 })
|
20
|
+
guard let index = bytes.firstIndex(where: { String(data: Data($0), encoding: .utf8) == key }),
|
21
|
+
let pwd = pwdBytes,
|
22
|
+
let value = decrypt(bytes[index + 1], password: pwd) else { return nil }
|
23
|
+
|
24
|
+
return String(data: Data(value), encoding: .utf8)
|
25
|
+
}
|
26
|
+
|
27
|
+
private func decrypt(_ input: [UInt8], password: [UInt8]) -> [UInt8]? {
|
28
|
+
guard !password.isEmpty else { return nil }
|
29
|
+
var output = [UInt8]()
|
30
|
+
for byte in input.enumerated() {
|
31
|
+
output.append(byte.element ^ password[byte.offset % password.count])
|
32
|
+
}
|
33
|
+
return output
|
34
|
+
}
|
35
|
+
|
36
|
+
<% if should_decrypt_files %>
|
37
|
+
func decryptFiles(bundle: Bundle = Bundle.main, password: String? = nil) throws {
|
38
|
+
try fileNames.forEach({ (fileNameBytes) in
|
39
|
+
guard let name = String(data: Data(fileNameBytes), encoding: .utf8) else {
|
40
|
+
fatalError("Wrong name in file names")
|
41
|
+
}
|
42
|
+
|
43
|
+
try decryptFile(name, bundle: bundle, password: password)
|
44
|
+
})
|
45
|
+
}
|
46
|
+
|
47
|
+
func decryptFile(_ fileName: String, bundle: Bundle = Bundle.main, password: String? = nil) throws {
|
48
|
+
let password = password == nil ? String(data: Data(bytes[0]), encoding: .utf8) : password
|
49
|
+
|
50
|
+
guard let pwd = password else {
|
51
|
+
fatalError("No password for decryption was provided!")
|
52
|
+
}
|
53
|
+
|
54
|
+
guard let filePath = bundle.path(forResource: fileName, ofType: "enc"),
|
55
|
+
let fileURL = URL(string: "file://" + filePath),
|
56
|
+
let fileData = try? Data(contentsOf: fileURL) else {
|
57
|
+
fatalError("File \(fileName) was not found in bundle!")
|
58
|
+
}
|
59
|
+
|
60
|
+
var outputURL = bundle.bundleURL
|
61
|
+
outputURL.appendPathComponent(fileName)
|
62
|
+
|
63
|
+
do {
|
64
|
+
let aes = try AES(keyString: pwd)
|
65
|
+
let decryptedString = try aes.decrypt(fileData)
|
66
|
+
try decryptedString.write(to: outputURL, atomically: true, encoding: .utf8)
|
67
|
+
} catch let e {
|
68
|
+
throw e
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
struct AES {
|
73
|
+
enum Error: Swift.Error {
|
74
|
+
case invalidKeySize
|
75
|
+
case encryptionFailed
|
76
|
+
case decryptionFailed
|
77
|
+
case dataToStringFailed
|
78
|
+
}
|
79
|
+
|
80
|
+
private var key: Data
|
81
|
+
private var ivSize: Int = kCCBlockSizeAES128
|
82
|
+
private let options: CCOptions = CCOptions()
|
83
|
+
|
84
|
+
init(keyString: String) throws {
|
85
|
+
guard keyString.count == kCCKeySizeAES256 else {
|
86
|
+
throw Error.invalidKeySize
|
87
|
+
}
|
88
|
+
self.key = Data(keyString.utf8)
|
89
|
+
}
|
90
|
+
|
91
|
+
func decrypt(_ data: Data) throws -> String {
|
92
|
+
let bufferSize: Int = data.count - ivSize
|
93
|
+
var buffer = Data(count: bufferSize)
|
94
|
+
var numberBytesDecrypted: Int = 0
|
95
|
+
|
96
|
+
do {
|
97
|
+
try key.withUnsafeBytes { keyBytes in
|
98
|
+
try data.withUnsafeBytes { dataToDecryptBytes in
|
99
|
+
try buffer.withUnsafeMutableBytes { bufferBytes in
|
100
|
+
|
101
|
+
guard let keyBytesBaseAddress = keyBytes.baseAddress,
|
102
|
+
let dataToDecryptBytesBaseAddress = dataToDecryptBytes.baseAddress,
|
103
|
+
let bufferBytesBaseAddress = bufferBytes.baseAddress else {
|
104
|
+
throw Error.encryptionFailed
|
105
|
+
}
|
106
|
+
|
107
|
+
let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot encrypt operation
|
108
|
+
CCOperation(kCCDecrypt), // op: CCOperation
|
109
|
+
CCAlgorithm(kCCAlgorithmAES), // alg: CCAlgorithm
|
110
|
+
options, // options: CCOptions
|
111
|
+
keyBytesBaseAddress, // key: the "password"
|
112
|
+
key.count, // keyLength: the "password" size
|
113
|
+
dataToDecryptBytesBaseAddress, // iv: Initialization Vector
|
114
|
+
dataToDecryptBytesBaseAddress + ivSize, // dataIn: Data to decrypt bytes
|
115
|
+
bufferSize, // dataInLength: Data to decrypt size
|
116
|
+
bufferBytesBaseAddress, // dataOut: decrypted Data buffer
|
117
|
+
bufferSize, // dataOutAvailable: decrypted Data buffer size
|
118
|
+
&numberBytesDecrypted // dataOutMoved: the number of bytes written
|
119
|
+
)
|
120
|
+
|
121
|
+
guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
|
122
|
+
throw Error.decryptionFailed
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
} catch {
|
128
|
+
throw Error.encryptionFailed
|
129
|
+
}
|
130
|
+
|
131
|
+
let decryptedData: Data = buffer[..<numberBytesDecrypted]
|
132
|
+
guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
|
133
|
+
throw Error.dataToStringFailed
|
134
|
+
}
|
135
|
+
|
136
|
+
return decryptedString
|
137
|
+
}
|
138
|
+
}
|
139
|
+
<% end %>
|
140
|
+
}
|
@@ -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
|
+
}
|
data/lib/resources/example.yml
CHANGED
@@ -1,7 +1,18 @@
|
|
1
1
|
MobileSecrets:
|
2
|
-
hashKey:
|
3
|
-
|
4
|
-
|
2
|
+
# hashKey: Key that will be used to hash the secret values.
|
3
|
+
# For encrypting files the key needs to be 32 chars long as an AES standard.
|
4
|
+
hashKey: "KokoBelloKokoKokoBelloKokoKokoBe"
|
5
|
+
# shouldIncludePassword: By default the password is saved in the code as a series of bytes, however it can also
|
6
|
+
# be fetched from your API, saved in keychain and passed to the Secrets for improving the security.
|
7
|
+
shouldIncludePassword: true
|
8
|
+
# language: Swift is currently only supported language, Kotlin is coming soon.
|
9
|
+
language: "Swift"
|
10
|
+
# Key-value dictionary for secrets. The key is then referenced in the code to get the secret.
|
11
|
+
secrets:
|
5
12
|
googleMaps: "123123123"
|
6
13
|
firebase: "asdasdasd"
|
7
14
|
amazon: "asd123asd123"
|
15
|
+
# Optional, remove files if you do not want to encrypt them
|
16
|
+
files:
|
17
|
+
- tmp.txt
|
18
|
+
- Info.plist
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module MobileSecrets
|
4
|
+
class FileHandler
|
5
|
+
|
6
|
+
def initialize password
|
7
|
+
@password = password
|
8
|
+
end
|
9
|
+
|
10
|
+
def encrypt file
|
11
|
+
file_content = File.read file
|
12
|
+
cipher = OpenSSL::Cipher::AES256.new :CBC
|
13
|
+
cipher.encrypt
|
14
|
+
iv = cipher.random_iv
|
15
|
+
cipher.key = @password
|
16
|
+
cipher_text = iv + cipher.update(file_content) + cipher.final
|
17
|
+
cipher_text
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/src/secrets_handler.rb
CHANGED
@@ -1,39 +1,46 @@
|
|
1
1
|
require "dotgpg"
|
2
2
|
require "yaml"
|
3
|
+
|
3
4
|
require_relative '../src/obfuscator'
|
5
|
+
require_relative '../src/file_handler'
|
6
|
+
require_relative '../src/source_renderer'
|
4
7
|
|
5
8
|
module MobileSecrets
|
6
9
|
class SecretsHandler
|
7
10
|
|
8
|
-
def export_secrets path
|
9
|
-
decrypted_config = decrypt_secrets()
|
10
|
-
|
11
|
-
|
11
|
+
def export_secrets path, from_encrypted_file_name
|
12
|
+
decrypted_config = decrypt_secrets(from_encrypted_file_name)
|
13
|
+
file_names_bytes, secrets_bytes = process_yaml_config decrypted_config
|
14
|
+
|
15
|
+
renderer = MobileSecrets::SourceRenderer.new "swift"
|
16
|
+
renderer.render_template secrets_bytes, file_names_bytes, "#{path}/secrets.swift"
|
17
|
+
decrypted_config
|
12
18
|
end
|
13
19
|
|
14
20
|
def process_yaml_config yaml_string
|
15
21
|
config = YAML.load(yaml_string)["MobileSecrets"]
|
16
22
|
hash_key = config["hashKey"]
|
17
|
-
obfuscator = MobileSecrets::Obfuscator.new hash_key
|
18
|
-
|
19
|
-
bytes = [hash_key.bytes]
|
20
23
|
secrets_dict = config["secrets"]
|
24
|
+
files = config["files"]
|
25
|
+
should_include_password = config["shouldIncludePassword"]
|
26
|
+
secrets_bytes = should_include_password ? [hash_key.bytes] : []
|
27
|
+
file_names_bytes = []
|
28
|
+
obfuscator = MobileSecrets::Obfuscator.new hash_key
|
21
29
|
|
22
30
|
secrets_dict.each do |key, value|
|
23
31
|
encrypted = obfuscator.obfuscate(value)
|
24
|
-
|
32
|
+
secrets_bytes << key.bytes << encrypted.bytes
|
25
33
|
end
|
26
34
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
swift_secrets = template.sub "/* SECRET BYTES */", bytes_variable
|
35
|
+
if files
|
36
|
+
abort("Password must be 32 characters long for files encryption.") if hash_key.length != 32
|
37
|
+
files.each do |f|
|
38
|
+
encrypt_file hash_key, f, "#{f}.enc"
|
39
|
+
file_names_bytes << f.bytes
|
40
|
+
end
|
41
|
+
end
|
35
42
|
|
36
|
-
|
43
|
+
return file_names_bytes, secrets_bytes
|
37
44
|
end
|
38
45
|
|
39
46
|
def encrypt output_file_path, string, gpg_path
|
@@ -43,12 +50,20 @@ module MobileSecrets
|
|
43
50
|
dotgpg.encrypt output_file_path, string
|
44
51
|
end
|
45
52
|
|
53
|
+
def encrypt_file password, file, output_file_path
|
54
|
+
encryptor = FileHandler.new password
|
55
|
+
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
|
56
|
+
encrypted_content = encryptor.encrypt file
|
57
|
+
|
58
|
+
File.open(output_file_path, "wb") { |f| f.write encrypted_content }
|
59
|
+
end
|
60
|
+
|
46
61
|
private
|
47
62
|
|
48
|
-
def decrypt_secrets
|
49
|
-
gpg = Dotgpg::Dir.
|
63
|
+
def decrypt_secrets encrypted_file_name
|
64
|
+
gpg = Dotgpg::Dir.closest encrypted_file_name
|
50
65
|
output = StringIO.new
|
51
|
-
gpg.decrypt "#{Dir.pwd}
|
66
|
+
gpg.decrypt "#{Dir.pwd}/#{encrypted_file_name}", output
|
52
67
|
output.string
|
53
68
|
end
|
54
69
|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
Book = Struct.new(:title, :author)
|
4
|
+
module MobileSecrets
|
5
|
+
class SourceRenderer
|
6
|
+
|
7
|
+
def initialize source_type
|
8
|
+
@source_type = source_type.downcase
|
9
|
+
end
|
10
|
+
|
11
|
+
def render_template secrets_bytes, file_names_bytes, output_file_path
|
12
|
+
template = ERB.new(File.read("#{__dir__}/../resources/SecretsSwift.erb"))
|
13
|
+
|
14
|
+
case @source_type
|
15
|
+
when "swift"
|
16
|
+
File.open(output_file_path, "w") do |file|
|
17
|
+
file.puts template.result_with_hash(secrets_array: secrets_bytes,
|
18
|
+
file_names_array: file_names_bytes,
|
19
|
+
should_decrypt_files: file_names_bytes.length > 0)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def render_empty_template output_file_path
|
25
|
+
template = File.read("#{__dir__}/../resources/SecretsSwiftEmpty.erb")
|
26
|
+
|
27
|
+
case @source_type
|
28
|
+
when "swift"
|
29
|
+
File.open(output_file_path, "w") do |file|
|
30
|
+
file.puts template
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Cermak
|
@@ -35,11 +35,13 @@ files:
|
|
35
35
|
- Rakefile
|
36
36
|
- bin/mobile-secrets
|
37
37
|
- lib/mobile-secrets.rb
|
38
|
-
- lib/resources/
|
38
|
+
- lib/resources/SecretsSwift.erb
|
39
|
+
- lib/resources/SecretsSwiftEmpty.erb
|
39
40
|
- lib/resources/example.yml
|
41
|
+
- lib/src/file_handler.rb
|
40
42
|
- lib/src/obfuscator.rb
|
41
43
|
- lib/src/secrets_handler.rb
|
42
|
-
-
|
44
|
+
- lib/src/source_renderer.rb
|
43
45
|
homepage: https://github.com/CyrilCermak/mobile-secrets
|
44
46
|
licenses:
|
45
47
|
- MIT
|
@@ -59,9 +61,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
61
|
- !ruby/object:Gem::Version
|
60
62
|
version: '0'
|
61
63
|
requirements: []
|
62
|
-
rubygems_version: 3.0.
|
64
|
+
rubygems_version: 3.0.8
|
63
65
|
signing_key:
|
64
66
|
specification_version: 4
|
65
67
|
summary: mobile-secrets tool for handling your mobile secrets
|
66
|
-
test_files:
|
67
|
-
- test/test_hola.rb
|
68
|
+
test_files: []
|
@@ -1,28 +0,0 @@
|
|
1
|
-
//
|
2
|
-
// Autogenerated file by Mobile Secrets
|
3
|
-
//
|
4
|
-
|
5
|
-
import Foundation
|
6
|
-
|
7
|
-
class Secrets {
|
8
|
-
static let standard = Secrets()
|
9
|
-
/* SECRET BYTES */
|
10
|
-
|
11
|
-
private init() {}
|
12
|
-
|
13
|
-
func string(forKey key: String) -> String? {
|
14
|
-
guard let index = bytes.firstIndex(where: { String(data: Data($0), encoding: .utf8) == key }),
|
15
|
-
let value = decrypt(bytes[index + 1]) else { return nil }
|
16
|
-
return String(data: Data(value), encoding: .utf8)
|
17
|
-
}
|
18
|
-
|
19
|
-
private func decrypt(_ input: [UInt8]) -> [UInt8]? {
|
20
|
-
let key = bytes[0]
|
21
|
-
guard !key.isEmpty else { return nil }
|
22
|
-
var output = [UInt8]()
|
23
|
-
for byte in input.enumerated() {
|
24
|
-
output.append(byte.element ^ key[byte.offset % key.count])
|
25
|
-
}
|
26
|
-
return output
|
27
|
-
}
|
28
|
-
}
|
data/test/test_hola.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
require 'test/unit'
|
2
|
-
require 'hola'
|
3
|
-
|
4
|
-
class HolaTest < Test::Unit::TestCase
|
5
|
-
def test_english_hello
|
6
|
-
assert_equal "hello world", Hola.hi("english")
|
7
|
-
end
|
8
|
-
|
9
|
-
def test_any_hello
|
10
|
-
assert_equal "hello world", Hola.hi("ruby")
|
11
|
-
end
|
12
|
-
|
13
|
-
def test_spanish_hello
|
14
|
-
assert_equal "hola mundo", Hola.hi("spanish")
|
15
|
-
end
|
16
|
-
end
|