scl 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.
@@ -0,0 +1,84 @@
1
+ require 'json'
2
+ module Scl
3
+ module Control
4
+ class DH < ControllerModule
5
+
6
+ help <<-HELP,
7
+ syn. Step 1 of a diffie hellman exchange.
8
+ Generates a private and public key and der to be used to generate a symmetric key with a second party.
9
+
10
+ e.g
11
+ >> Using public key (Saves to [filename].enc by default, unless -o is used)
12
+ scl dh syn
13
+ scl dh syn -fbase64
14
+ scl dh syn -o ./dhell
15
+ HELP
16
+ def syn
17
+ syn = dh.syn
18
+ args.output_file ||= "dh-key-exchange"
19
+ controller.output(
20
+ Output.new(syn[:private].to_json, ".dh1.priv"),
21
+ Output.new(syn[:public].to_json, ".dh1.pub")
22
+ )
23
+ end
24
+
25
+ help <<-HELP,
26
+ ack. Step 2 of a diffie hellman exchange.
27
+ Requires the public output of step 1 (The der and public key)
28
+ Generates a private and public key and der to be used to generate a symmetric key with a second party.
29
+
30
+ The output [output_file].key contains the secret key generated by the DH exchange.
31
+ The public output [output_file].dh2.pub must be passed back to the first party to complete the exchange
32
+ e.g
33
+ >> Using public key (Saves to [filename].enc by default, unless -o is used)
34
+ scl dh ack ./dh-key-exchange.dh1.pub
35
+ scl dh ack ./dhell.dh1.pub -o ./dhell
36
+ HELP
37
+ def ack(file)
38
+ unless File.exists?(file)
39
+ raise ControlError.new("Couldn't find file containing part 1 of diffie hellman exchange: #{file}. File doesnt exist")
40
+ end
41
+ input = JSON.parse(input_decoder.decode(IO.read(file)))
42
+ ack = dh.ack(der: input['der'], public_key: input['public_key'])
43
+ args.output_file ||= "dh-key-exchange"
44
+ controller.output(
45
+ Output.new(ack[:private].to_json, ".dh2.key"),
46
+ Output.new(ack[:public].to_json, ".dh2.pub")
47
+ )
48
+ end
49
+
50
+ help <<-HELP,
51
+ fin. Step 3 and final step of a diffie hellman exchange.
52
+ Requires the private output of step 1 (The der and private key) and the public key from step 2
53
+ Generates a secret key shared with the second party
54
+
55
+ The output [output_file].key contains the secret key generated by the DH exchange.
56
+
57
+ e.g
58
+ >> Using public key (Saves to [filename].enc by default, unless -o is used)
59
+ scl dh fin ./dh-key-exchange.dh1.priv ./dh-key-exchange.dh2.pub
60
+ scl dh fin ./dhell.dh1.priv ./dhell.dh2.pub -o ./dhell
61
+ HELP
62
+ def fin(dh1_priv, dh2_pub)
63
+ unless File.exists?(dh1_priv)
64
+ raise ControlError.new("Couldn't find file private portion of part 1 of diffie hellman exchange: #{dh1_priv}. File doesnt exist")
65
+ end
66
+ unless File.exists?(dh2_pub)
67
+ raise ControlError.new("Couldn't find file containing public portion of part 2 of diffie hellman exchange: #{dh2_pub}. File doesnt exist")
68
+ end
69
+ dh1 = JSON.parse(input_decoder.decode(IO.read(dh1_priv)))
70
+ dh2 = JSON.parse(input_decoder.decode(IO.read(dh2_pub)))
71
+ result = dh.fin(der: dh1['der'], private_key: dh1['private_key'], public_key: dh2['public_key'])
72
+ args.output_file ||= "dh-key-exchange"
73
+ controller.output(
74
+ Output.new(result[:private].to_json, ".dh1.key")
75
+ )
76
+ end
77
+
78
+ private
79
+ def dh
80
+ @dh ||= Scl::DH.new
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,90 @@
1
+ module Scl
2
+ module Control
3
+ class Digest < ControllerModule
4
+
5
+ help <<-HELP,
6
+ sign. Signs a file using a support hash algorithm. Defaults to sha256
7
+ e.g
8
+ scl digest sign /path/to/file
9
+ scl digest sign /path/to/file -o stdout
10
+ scl digest sign /path/to/file -d sha512
11
+ HELP
12
+ def sign(input_file)
13
+ signature = Scl::Digest.digest(args.digest || 'sha256', read_file(input_file, "File to sign"))
14
+ args.output_file ||= input_file
15
+ args.output_format ||= 'binary'
16
+ controller.output(
17
+ Output.new(signature, ".sig")
18
+ )
19
+ end
20
+
21
+ help <<-HELP,
22
+ verify
23
+ e.g.
24
+ scl digest verify file signature
25
+ scl digest verify file signature -d sha512
26
+ HELP
27
+ def verify(input_file, signature_file)
28
+ signature = Scl::Digest.digest(args.digest || 'sha256', read_file(input_file, "File to verify"))
29
+ args.input_format ||= 'binary'
30
+ if signature == input_decoder.decode(read_file(signature_file, "Signature"))
31
+ exit(0)
32
+ else
33
+ exit(1)
34
+ end
35
+ end
36
+
37
+ help <<-HELP,
38
+ hmac. Generates an HMAC for a file, can be given an optional key or alternately one will be
39
+ generated for you. Keep hold of this key for verifying signatures in the future
40
+ e.g.
41
+ scl hmac file # Generates both a digest and a secure random key
42
+ scl hmac file -k keyfile # Generates a digest using an existing key
43
+ scl hmac file -d sha512 # Provide alternate digest algorithm
44
+ HELP
45
+ def hmac(input_file)
46
+ input_key = args.key_path ?
47
+ read_file(args.key_path) :
48
+ nil
49
+ signature, key = Scl::Digest.hmac(args.digest || 'sha256', read_file(input_file, "File to sign"), input_key)
50
+ args.output_file ||= input_file
51
+ args.output_format ||= 'binary'
52
+ controller.output(
53
+ Output.new(signature, ".sig"),
54
+ input_key ? nil : Output.new(key, '.key')
55
+ )
56
+ end
57
+
58
+ help <<-HELP,
59
+ hmac. Verifies the contents of a file match an HMAC signature (using a given key)
60
+ e.g.
61
+ scl hmac_verify file signature -k keyfile
62
+ HELP
63
+ def hmac_verify(input_file, signature_file)
64
+ args.input_format ||= 'binary'
65
+ key = input_decoder.decode(read_file(args.key_path, 'HMAC Key file', 'Use -k'))
66
+ data = read_file(input_file, "File to verify")
67
+ signature, key = Scl::Digest.hmac(args.digest || 'sha256', data, key)
68
+ if signature == input_decoder.decode(read_file(signature_file, "Signature"))
69
+ exit(0)
70
+ else
71
+ exit(1)
72
+ end
73
+ end
74
+
75
+ help <<-HELP,
76
+ list. List supported hash algorithms
77
+ e.g.
78
+ scl digest list
79
+ HELP
80
+ def list
81
+ puts OpenSSL::Digest.constants.reject{|x| x.to_s =~ /Error/ }
82
+ end
83
+
84
+ private
85
+ def ss
86
+ @ss ||= Scl::SecretShare.new(args.min_shares.to_i, args.num_shares.to_i)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,14 @@
1
+ module Scl
2
+ module Control
3
+ class Output
4
+ attr_reader :content
5
+ def initialize(content, suffix)
6
+ @content, @suffix = content, suffix
7
+ end
8
+
9
+ def file(path)
10
+ File.join("#{path.gsub(%r(#{@suffix.gsub('.','\.')}$),'')}#{@suffix}")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,161 @@
1
+ module Scl
2
+ module Control
3
+ class RSA < ControllerModule
4
+
5
+ help <<-HELP,
6
+ Generates an RSA keypair
7
+ e.g
8
+ >> Generates a key-pair and prints to stdout
9
+ $ scl rsa generate
10
+
11
+ >> Generates a key-pair and saves to the filesystem
12
+ $ scl rsa generate -o /path/to/file
13
+ HELP
14
+ def generate
15
+ begin
16
+ result = Scl::RSA.generate(args.key_size)
17
+ controller.output(
18
+ Output.new(result.public.export, '.pub'),
19
+ Output.new(result.private.export, '.priv')
20
+ )
21
+ rescue StandardError => e
22
+ raise ControlError.new("Couldn't generate key of size #{args.key_size}")
23
+ end
24
+ end
25
+
26
+ help <<-HELP,
27
+ Signs a file using an RSA private key.
28
+ This file can then be verified using the corresponding public-key or the same private-key
29
+ e.g
30
+ >> Sign [file] using private key
31
+ $ scl rsa sign file -Z /path/to/private/key
32
+ HELP
33
+ def sign(file)
34
+ unless args.private_key_file
35
+ raise ControlError.new("Please provide a private key file (-Z or --priv) use --help to find out more")
36
+ end
37
+ unless File.exists?(args.private_key_file)
38
+ raise ControlError.new("Private key file #{args.private_key_file} doesnt exist")
39
+ end
40
+ unless File.exists?(file)
41
+ raise ControlError.new("Couldn't find file to sign: #{file}. File doesnt exist")
42
+ end
43
+
44
+ private_key = load_key(args.private_key_file)
45
+ signature = private_key.sign(IO.read(file))
46
+ args.output_file ||= file
47
+ controller.output(
48
+ Output.new(signature, ".sig")
49
+ )
50
+ end
51
+
52
+ help <<-HELP,
53
+ Verifies a file matches a signature for an RSA private key
54
+ Verification can be performed using the corresponding public-key or the same private-key that generated the signature
55
+ e.g
56
+
57
+ >> Verify [file] using public key
58
+ $ scl rsa verify -p /path/to/private/key file file.sig
59
+ $ scl rsa verify --pub-key /path/to/private/key file file.sig
60
+
61
+ >> Verify [file] using private key
62
+ $ scl rsa verify -Z /path/to/private/key file file.sig
63
+ HELP
64
+ def verify(file, signature="#{file}.sig")
65
+ key_file = args.public_key_file || args.private_key_file
66
+ unless key_file
67
+ raise ControlError.new("Please provide a private or public key file (-p --pub-key, -Z or --priv-key) use --help to find out more")
68
+ end
69
+ unless File.exists?(key_file)
70
+ raise ControlError.new("Key file #{key_file} doesnt exist")
71
+ end
72
+ unless File.exists?(file)
73
+ raise ControlError.new("Couldn't find file to verify: #{file}. File doesnt exist")
74
+ end
75
+ unless File.exists?(signature)
76
+ raise ControlError.new("Couldn't find signature to verify: #{signature}. File doesnt exist")
77
+ end
78
+ key = load_key(key_file)
79
+ if key.verify(input_decoder.decode(IO.read(signature)), IO.read(file))
80
+ exit(0)
81
+ else
82
+ exit(1)
83
+ end
84
+ end
85
+
86
+ help <<-HELP,
87
+ Encrypts a file.
88
+ e.g
89
+ >> Using public key (Saves to [filename].enc by default, unless -o is used)
90
+ scl rsa encrypt -p /path/to/public_key /path/to/file
91
+ scl rsa encrypt --pub-key /path/to/public_key /path/to/file
92
+
93
+ >> Using public key (Saves to [filename].enc by default, unless -o is used)
94
+ scl rsa encrypt -Z /path/to/private_key /path/to/file
95
+ scl rsa encrypt --priv-key /path/to/private_key /path/to/file
96
+ HELP
97
+ def encrypt(file)
98
+ key_file = args.public_key_file || args.private_key_file
99
+ unless key_file
100
+ raise ControlError.new("Please provide a private or public key file (-p --pub-key, -Z or --priv-key) use --help to find out more")
101
+ end
102
+ unless File.exists?(key_file)
103
+ raise ControlError.new("Key file #{key_file} doesnt exist")
104
+ end
105
+ unless File.exists?(file)
106
+ raise ControlError.new("Couldn't find file to verify: #{file}. File doesnt exist")
107
+ end
108
+ key = load_key(key_file)
109
+ encrypted = key.encrypt(IO.read(file))
110
+ args.output_file ||= file
111
+ controller.output(
112
+ Output.new(encrypted, '.enc')
113
+ )
114
+ end
115
+
116
+ help <<-HELP,
117
+ Decrypts a file.
118
+ e.g
119
+ >> Using public key (Writes to stdout unless -o is used)
120
+ scl rsa decrypt -p /path/to/public_key /path/to/file.enc
121
+ scl rsa decrypt --pub-key /path/to/public_key /path/to/file.enc
122
+
123
+ >> Using public key (Writes to stdout unless -o is used)
124
+ scl rsa decrypt -Z /path/to/private_key /path/to/file.enc
125
+ scl rsa decrypt --priv-key /path/to/private_key /path/to/file.enc
126
+ HELP
127
+ def decrypt(file)
128
+ key_file = args.public_key_file || args.private_key_file
129
+ unless key_file
130
+ raise ControlError.new("Please provide a private or public key file (-p --pub-key, -Z or --priv-key) use --help to find out more")
131
+ end
132
+ unless File.exists?(key_file)
133
+ raise ControlError.new("Key file #{key_file} doesnt exist")
134
+ end
135
+ unless File.exists?(file)
136
+ raise ControlError.new("Couldn't find file to verify: #{file}. File doesnt exist")
137
+ end
138
+ key = load_key(key_file)
139
+ decrypted = key.decrypt(input_decoder.decode(IO.read(file)))
140
+ args.output_format = 'binary'
141
+ controller.output(
142
+ Output.new(decrypted, '')
143
+ )
144
+ end
145
+
146
+ private
147
+ def load_key(key_file)
148
+ raw_input = IO.read(key_file)
149
+ puts "Decoding key using #{input_decoder.name}" if args.verbose
150
+ decoded_input = key_coder.decode(raw_input)
151
+ puts "Constructing new RSA key using decoded input" if args.verbose
152
+ begin
153
+ pkey = OpenSSL::PKey::RSA.new(decoded_input)
154
+ Scl::RSA::Key.new(pkey)
155
+ rescue StandardError => e
156
+ raise ControlError.new("Unable to construct key from decoded input #{e.message}")
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,48 @@
1
+ module Scl
2
+ module Control
3
+ class SSS < ControllerModule
4
+
5
+ help <<-HELP,
6
+ generate. Generate a set of shares for a shared secret
7
+ Arguments are the total number of shares to generate, and the number of shares required to unlock
8
+ the secret.
9
+ e.g
10
+ scl sss generate -m 3 -n 5
11
+ scl sss generate --min-shares=11 --num-shares=14 -o output_file
12
+
13
+ Large secrets are encoded using multiple blocks, which can create large shares.
14
+ An alternative, more space efficient approach to this is to encode a shorter key using secret sharing,
15
+ and to then encrypt the large secret using this key and a block-cipher
16
+
17
+ HELP
18
+ def generate(input_file)
19
+ raise ControlError.new("Min-shares must be a positive integer") unless args.min_shares.to_i > 0
20
+ raise ControlError.new("Num-shares must be a positive integer") unless args.num_shares.to_i > 0
21
+ raise ControlError.new('Num shares must be larger than or equal to min shares') unless args.num_shares.to_i >= args.min_shares.to_i
22
+ input = read_file(input_file)
23
+ args.output_file ||= input_file
24
+ controller.output(
25
+ Output.new(ss.generate(input).join("\n"), ".shares")
26
+ )
27
+ end
28
+
29
+ help <<-HELP,
30
+ combine. Combines a set of shares for a shared secret
31
+ Expects as an argument a file of "\n" separate secret shares
32
+ e.g
33
+ scl sss combine shares.txt
34
+ HELP
35
+ def combine(input_file)
36
+ shares = read_file(input_file).split("\n")
37
+ controller.output(
38
+ Output.new(Scl::SecretShare.combine(shares), ".sec")
39
+ )
40
+ end
41
+
42
+ private
43
+ def ss
44
+ @ss ||= Scl::SecretShare.new(args.min_shares.to_i, args.num_shares.to_i)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ require 'scl/control/output'
2
+ require 'scl/control/controller'
3
+ require 'scl/control/controller_module'
4
+ require 'scl/control/aes'
5
+ require 'scl/control/rsa'
6
+ require 'scl/control/sss'
7
+ require 'scl/control/dh'
8
+ require 'scl/control/digest'
9
+
10
+ module Scl
11
+ module Control
12
+ class ControlError < StandardError; end
13
+ end
14
+ end
data/lib/scl/dh.rb ADDED
@@ -0,0 +1,52 @@
1
+ module Scl
2
+ require 'base64'
3
+ class DH
4
+ attr_reader :encoder
5
+
6
+ def initialize(encoder: Format::BASE64)
7
+ @encoder = encoder
8
+ end
9
+ # :0> syn = Scl::DH.new.syn
10
+ # :1> ack = Scl::DH.new.ack(syn[:public])
11
+ # :2> shared_key1 = Scl::DH.new.fin(syn[:private].merge(ack[:public]))[:private][:shared_key]
12
+ # :3> shared_key2 = ack[:private][:shared_key]
13
+ def syn(length: 512)
14
+ dh = OpenSSL::PKey::DH.new(length)
15
+ {
16
+ private: {
17
+ der: encoder.encode(dh.public_key.to_der),
18
+ private_key: encoder.encode(dh.priv_key.to_s(16))
19
+ },
20
+ public: {
21
+ der: encoder.encode(dh.public_key.to_der),
22
+ public_key: encoder.encode(dh.pub_key.to_s(16))
23
+ }
24
+ }
25
+ end
26
+
27
+ def ack(der:, public_key:)
28
+ dh = OpenSSL::PKey::DH.new(encoder.decode(der))
29
+ dh.generate_key!
30
+ shared_key = dh.compute_key(OpenSSL::BN.new(encoder.decode(public_key), 16))
31
+ {
32
+ private: {
33
+ shared_key: encoder.encode(shared_key)
34
+ },
35
+ public: {
36
+ public_key: encoder.encode(dh.pub_key.to_s(16))
37
+ }
38
+ }
39
+ end
40
+
41
+ def fin(private_key:, der:, public_key:)
42
+ dh = OpenSSL::PKey::DH.new(encoder.decode(der))
43
+ dh.priv_key = OpenSSL::BN.new(encoder.decode(private_key), 16)
44
+ shared_key = dh.compute_key(OpenSSL::BN.new(encoder.decode(public_key), 16))
45
+ {
46
+ private: {
47
+ shared_key: encoder.encode(shared_key)
48
+ }
49
+ }
50
+ end
51
+ end
52
+ end
data/lib/scl/digest.rb ADDED
@@ -0,0 +1,29 @@
1
+ module Scl
2
+ class Digest
3
+ def self.digest(digest, data)
4
+ if digest_exists?(digest)
5
+ OpenSSL::Digest.const_get(digest.upcase).new.hexdigest(data)
6
+ end
7
+ end
8
+
9
+ def self.hmac(digest, data, key=nil)
10
+ if digest_exists?(digest)
11
+ require 'securerandom'
12
+ key = key || SecureRandom.hex
13
+ [OpenSSL::HMAC.hexdigest(digest, key, data), key]
14
+ end
15
+ end
16
+
17
+ private
18
+ def self.digest_exists?(digest)
19
+ begin
20
+ OpenSSL::Digest.const_get(digest.upcase)
21
+ rescue NameError => e
22
+ puts "Couldn't get digest type. #{digest}"
23
+ puts "Try $ scl digest list – for a list of supported digests"
24
+ return false
25
+ end
26
+ true
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module Scl
2
+ class Scl::Auto < Scl::Format
3
+ def encode(data)
4
+ sorted = (data.chars.map(&:ord).uniq.sort) - [10]
5
+ if sorted.first < 32 || sorted.max > 126
6
+ Scl::Format::BASE64.encode(data)
7
+ else
8
+ Scl::Format::BINARY.encode(data)
9
+ end
10
+ end
11
+
12
+ def decode(data)
13
+ png = Regexp.new("\x89PNG".force_encoding("binary"))
14
+ if /^#{png}/ === data
15
+ Scl::Format::QRCODE.decode(data)
16
+ elsif data[/[^A-Za-z0-9\+\/\n=]/]
17
+ Scl::Format::BINARY.decode(data)
18
+ else
19
+ Scl::Format::BASE64.decode(data)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ module Scl
2
+ class Scl::Base64 < Scl::Format
3
+ require 'base64'
4
+ def encode(data)
5
+ ::Base64.encode64(data)
6
+ end
7
+
8
+ def decode(data)
9
+ ::Base64.decode64(data)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module Scl
2
+ class Scl::Binary < Scl::Format
3
+ def encode(data)
4
+ data
5
+ end
6
+
7
+ def decode(data)
8
+ data
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,44 @@
1
+ module Scl
2
+ class Format
3
+ def output(filename, data)
4
+ IO.write(filename, encode(data))
5
+ end
6
+
7
+ def read(filename)
8
+ decode(IO.read(filename))
9
+ end
10
+
11
+ def encode(data)
12
+ raise "Must be implemented by subclass"
13
+ end
14
+
15
+ def decode(data)
16
+ raise "Must be implemented by subclass"
17
+ end
18
+
19
+ def name
20
+ self.class.name
21
+ end
22
+ end
23
+ end
24
+
25
+ require 'scl/formats/base64'
26
+ require 'scl/formats/binary'
27
+ require 'scl/formats/words'
28
+ require 'scl/formats/auto'
29
+ require 'scl/formats/hex'
30
+ require 'scl/formats/qrcode'
31
+ require 'scl/formats/stdout'
32
+
33
+ module Scl
34
+ class Format
35
+
36
+ BASE64 = Scl::Base64.new
37
+ BINARY = Scl::Binary.new
38
+ WORDS = Scl::Words.new
39
+ QRCODE = Scl::QRCode.new
40
+ HEX = Scl::Hex.new
41
+ AUTO = Scl::Auto.new
42
+ STDOUT = Scl::Stdout.new
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ module Scl
2
+ class Scl::Hex < Scl::Format
3
+ def encode(data)
4
+ data.bytes.map{|x| x.to_s(16).rjust(2, ?0) }.join
5
+ end
6
+
7
+ def decode(data)
8
+ data.scan(/../).map{|s| s.to_i(16) }.map(&:chr).join
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Scl
2
+ class Scl::QRCode < Scl::Format
3
+ def encode(data)
4
+ raise "Not implemented yet"
5
+ end
6
+
7
+ def decode(data)
8
+ raise "Not implemented yet"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Scl
2
+ class Scl::Stdout < Scl::Format
3
+ def encode(data)
4
+ puts data
5
+ end
6
+
7
+ def decode(data)
8
+ data
9
+ end
10
+ end
11
+ end