prototok 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 +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +14 -0
- data/.travis.yml +5 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +7 -0
- data/Rakefile +6 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/prototok/attribute.rb +21 -0
- data/lib/prototok/ciphers/V1/encrypted_mac.rb +24 -0
- data/lib/prototok/ciphers/V1/encrypted_sign.rb +34 -0
- data/lib/prototok/ciphers/V1/mac.rb +24 -0
- data/lib/prototok/ciphers/V1/sign.rb +34 -0
- data/lib/prototok/ciphers/v1.rb +7 -0
- data/lib/prototok/ciphers.rb +23 -0
- data/lib/prototok/config.rb +25 -0
- data/lib/prototok/encoders/json.rb +23 -0
- data/lib/prototok/encoders/msgpack.rb +23 -0
- data/lib/prototok/encoders/protobuf/token.proto +11 -0
- data/lib/prototok/encoders/protobuf.rb +75 -0
- data/lib/prototok/encoders.rb +51 -0
- data/lib/prototok/errors.rb +25 -0
- data/lib/prototok/formatters/default.rb +17 -0
- data/lib/prototok/formatters.rb +7 -0
- data/lib/prototok/serializers/time.rb +15 -0
- data/lib/prototok/serializers/token.rb +8 -0
- data/lib/prototok/serializers.rb +146 -0
- data/lib/prototok/token.rb +36 -0
- data/lib/prototok/utils/listed.rb +19 -0
- data/lib/prototok/utils/paths.rb +11 -0
- data/lib/prototok/utils/protoc.rb +43 -0
- data/lib/prototok/utils/test_helper.rb +74 -0
- data/lib/prototok/utils/type_attributes.rb +19 -0
- data/lib/prototok/utils.rb +5 -0
- data/lib/prototok/version.rb +3 -0
- data/lib/prototok.rb +58 -0
- data/prototok.gemspec +34 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: aa2f4a8d086b1a650e523810806c1ab4bc415fa6fe1a24e862465cdbe2fab533
|
4
|
+
data.tar.gz: 8368dade9383592cc2188594cfc000791eca34c13ab759c812be087e74441a6d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e3638dd67c842af17c23225f74d023d572543e7fb39767a642e77c754cf49694422ddb33c96f417651f3c0b534b0b8e77ec4063346b330c039711c3d55770ead
|
7
|
+
data.tar.gz: 5825094f34c5bd8d9b0e2a7c0c6b872009416024724fb3269294d3023b31f684c7b39f7ccafbd08ee59aff437ef77d7da7cd4c1f42b7e62ef68a16c394afbdef
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Kostrov Alexander
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "prototok"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
data/bin/setup
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Serializers
|
3
|
+
class Attribute
|
4
|
+
attr_reader :options
|
5
|
+
attr_reader :serializer
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@options = options || {}
|
9
|
+
@serializer = Serializers.find(@options[:serializer])
|
10
|
+
end
|
11
|
+
|
12
|
+
def serialize(value)
|
13
|
+
if @serializer
|
14
|
+
@serializer.new(value).encode
|
15
|
+
else
|
16
|
+
value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rbnacl/secret_boxes/xsalsa20poly1305'
|
2
|
+
|
3
|
+
module Prototok
|
4
|
+
module Ciphers
|
5
|
+
module V1
|
6
|
+
class EncryptedMac < Base
|
7
|
+
self.cipher_class = RbNaCl::SecretBoxes::XSalsa20Poly1305
|
8
|
+
|
9
|
+
def initialize(private_key)
|
10
|
+
@cipher = cipher_class.new(private_key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def encode(blob)
|
14
|
+
nonce = RbNaCl::Random.random_bytes @cipher.nonce_bytes
|
15
|
+
[nonce, @cipher.box(nonce, blob)]
|
16
|
+
end
|
17
|
+
|
18
|
+
def decode(decoded_nonce, decoded_blob)
|
19
|
+
@cipher.open(decoded_nonce, decoded_blob)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rbnacl/boxes/curve25519xsalsa20poly1305'
|
2
|
+
|
3
|
+
module Prototok
|
4
|
+
module Ciphers
|
5
|
+
module V1
|
6
|
+
class EncryptedSign < Base
|
7
|
+
self.cipher_class = RbNaCl::Boxes::Curve25519XSalsa20Poly1305
|
8
|
+
|
9
|
+
def initialize(private_key, remote_public_key)
|
10
|
+
@private_key = private_key
|
11
|
+
@remote_public_key = remote_public_key
|
12
|
+
@cipher = cipher_class.new(@remote_public_key, @private_key)
|
13
|
+
end
|
14
|
+
|
15
|
+
def encode(blob)
|
16
|
+
nonce = RbNaCl::Random.random_bytes(cipher_class.nonce_bytes)
|
17
|
+
[nonce, @cipher.encrypt(nonce, blob)]
|
18
|
+
end
|
19
|
+
|
20
|
+
def decode(decoded_nonce, decoded_blob)
|
21
|
+
@cipher.decrypt(decoded_nonce, decoded_blob)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.key private_key=nil
|
25
|
+
if private_key.nil?
|
26
|
+
cipher_class::PrivateKey.generate.to_bytes
|
27
|
+
else
|
28
|
+
cipher_class::PrivateKey.new(private_key).public_key.to_bytes
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rbnacl/hmac/sha512256'
|
2
|
+
|
3
|
+
module Prototok
|
4
|
+
module Ciphers
|
5
|
+
module V1
|
6
|
+
class Mac < Base
|
7
|
+
self.cipher_class = RbNaCl::HMAC::SHA512256
|
8
|
+
|
9
|
+
def initialize(private_key)
|
10
|
+
@cipher = cipher_class.new(private_key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def encode(blob)
|
14
|
+
[@cipher.auth(blob), blob]
|
15
|
+
end
|
16
|
+
|
17
|
+
def decode(decoded_auth, decoded_blob)
|
18
|
+
@cipher.verify(decoded_auth, decoded_blob)
|
19
|
+
decoded_blob
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rbnacl/signatures/ed25519'
|
2
|
+
|
3
|
+
module Prototok
|
4
|
+
module Ciphers
|
5
|
+
module V1
|
6
|
+
class Sign < Base
|
7
|
+
self.cipher_class = RbNaCl::Signatures::Ed25519
|
8
|
+
|
9
|
+
def initialize(private_or_public_key)
|
10
|
+
@key = private_or_public_key
|
11
|
+
end
|
12
|
+
|
13
|
+
def encode(blob)
|
14
|
+
cipher = cipher_class::SigningKey.new(@key)
|
15
|
+
[cipher.sign(blob), blob]
|
16
|
+
end
|
17
|
+
|
18
|
+
def decode(decoded_auth, decoded_blob)
|
19
|
+
cipher = cipher_class::VerifyKey.new(@key)
|
20
|
+
cipher.verify(decoded_auth, decoded_blob)
|
21
|
+
decoded_blob
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.key(private_key = nil)
|
25
|
+
if private_key.nil?
|
26
|
+
cipher_class::SigningKey.generate.to_bytes
|
27
|
+
else
|
28
|
+
cipher_class::SigningKey.new(private_key).verify_key.to_bytes
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Ciphers
|
3
|
+
Autoloaded.class {}
|
4
|
+
extend Utils::Listed
|
5
|
+
class Base
|
6
|
+
class << self
|
7
|
+
attr_writer :cipher_class
|
8
|
+
def cipher_class
|
9
|
+
@cipher_class ||
|
10
|
+
raise(Errors::CipherError, 'No cipher_class declared')
|
11
|
+
end
|
12
|
+
|
13
|
+
def key
|
14
|
+
RbNaCl::Random.random_bytes(cipher_class.key_bytes)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def cipher_class
|
19
|
+
self.class.cipher_class
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Prototok
|
2
|
+
CONFIG_DEFAULTS = {
|
3
|
+
formatter: :default,
|
4
|
+
version: 1,
|
5
|
+
op: :encrypted_mac,
|
6
|
+
encoder: :json,
|
7
|
+
token_delimiter: '.',
|
8
|
+
encoder_options: {
|
9
|
+
encoding_mode: :token
|
10
|
+
},
|
11
|
+
time_encoding_precision: 10
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configuration
|
16
|
+
@configutation ||= CONFIG_DEFAULTS.dup
|
17
|
+
end
|
18
|
+
|
19
|
+
def configure
|
20
|
+
yield(configuration)
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :config, :configuration
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module Prototok
|
4
|
+
module Encoders
|
5
|
+
class Json < Base
|
6
|
+
def encode_token payload, **header
|
7
|
+
MultiJson.encode serialize(payload, **header)
|
8
|
+
end
|
9
|
+
|
10
|
+
def decode_token str
|
11
|
+
deserialize(MultiJson.decode(str))
|
12
|
+
end
|
13
|
+
|
14
|
+
def encode_payload payload
|
15
|
+
MultiJson.encode payload.to_h
|
16
|
+
end
|
17
|
+
|
18
|
+
def decode_payload str
|
19
|
+
MultiJson.decode(str)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
|
3
|
+
module Prototok
|
4
|
+
module Encoders
|
5
|
+
class Msgpack < Base
|
6
|
+
def encode_token payload, **header
|
7
|
+
MessagePack.pack serialize(payload, **header)
|
8
|
+
end
|
9
|
+
|
10
|
+
def decode_token str
|
11
|
+
deserialize(MessagePack.unpack(str))
|
12
|
+
end
|
13
|
+
|
14
|
+
def encode_payload payload
|
15
|
+
MessagePack.pack payload.to_h
|
16
|
+
end
|
17
|
+
|
18
|
+
def decode_payload str
|
19
|
+
MessagePack.unpack(str)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'google/protobuf'
|
2
|
+
require 'google/protobuf/well_known_types'
|
3
|
+
|
4
|
+
module Prototok
|
5
|
+
module Encoders
|
6
|
+
class Protobuf < Base
|
7
|
+
PROTOBUF_DEFAULTS = {
|
8
|
+
payload_class: '::Prototok::Protobuf::Payload'
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
def encode_token payload, **header
|
12
|
+
serialized = serialize(payload, **header)
|
13
|
+
prepared_token = prepare_token(serialized)
|
14
|
+
prepared_token.class.encode(prepared_token)
|
15
|
+
end
|
16
|
+
|
17
|
+
def decode_token str
|
18
|
+
decoded_token = Prototok::Protobuf::Token.decode(str)
|
19
|
+
payload = decoded_token.payload.unpack(payload_class)
|
20
|
+
token = deserialize(decoded_token.to_h)
|
21
|
+
token.payload = payload
|
22
|
+
token
|
23
|
+
end
|
24
|
+
|
25
|
+
def encode_payload payload
|
26
|
+
payload = payload_class.new(payload.to_h.reject { |_, v| v.nil? })
|
27
|
+
payload_class.encode(payload)
|
28
|
+
end
|
29
|
+
|
30
|
+
def decode_payload str
|
31
|
+
payload_class.decode(str)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.options
|
35
|
+
@options ||= super.merge!(PROTOBUF_DEFAULTS)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def prepare_token serialized_token
|
41
|
+
payload = payload_class.new(serialized_token[:payload] || {})
|
42
|
+
any = Google::Protobuf::Any.new
|
43
|
+
any.pack payload
|
44
|
+
serialized_token[:payload] = any
|
45
|
+
Prototok::Protobuf::Token.new(serialized_token)
|
46
|
+
end
|
47
|
+
|
48
|
+
def payload_class
|
49
|
+
self.class.payload_class(options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.payload_class(opts)
|
53
|
+
@cache ||= {}
|
54
|
+
@cache[opts] ||= begin
|
55
|
+
existing = try_get_existed(opts[:payload_class])
|
56
|
+
return existing unless existing.nil?
|
57
|
+
unless opts[:payload_file]
|
58
|
+
Prototok.send :err, Errors::ConfigurationError, 'no_payload_proto_file'
|
59
|
+
end
|
60
|
+
Prototok::Utils::Protoc.process(opts[:payload_file])
|
61
|
+
Object.const_get opts[:payload_class], false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.try_get_existed(klass_name)
|
66
|
+
Object.const_get klass_name, false
|
67
|
+
rescue NameError
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
base_token = File.join(__dir__, 'protobuf/token.proto')
|
72
|
+
Prototok::Utils::Protoc.process(base_token)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Encoders
|
3
|
+
Autoloaded.class {}
|
4
|
+
extend Utils::Listed
|
5
|
+
|
6
|
+
class Base
|
7
|
+
def options
|
8
|
+
@options ||= self.class.options.dup
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.options
|
12
|
+
@options ||= Prototok.config[:encoder_options].dup
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(**encoder_options)
|
16
|
+
options.merge!(encoder_options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def encode payload, **header
|
20
|
+
case options[:encoding_mode].to_s
|
21
|
+
when 'token'
|
22
|
+
encode_token payload, **header
|
23
|
+
when 'payload'
|
24
|
+
encode_payload payload
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def decode str
|
29
|
+
case options[:encoding_mode].to_s
|
30
|
+
when 'token'
|
31
|
+
decode_token str
|
32
|
+
when 'payload'
|
33
|
+
decode_payload str
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def serialize payload=nil, **header
|
38
|
+
if payload.is_a? Token
|
39
|
+
token = payload.dup.update!(header)
|
40
|
+
else
|
41
|
+
token = Token.new.update!(header.merge(:payload => payload))
|
42
|
+
end
|
43
|
+
Serializers.find(:token).new(token).encode
|
44
|
+
end
|
45
|
+
|
46
|
+
def deserialize data
|
47
|
+
Token.new(Serializers.find(:token).decode (data))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Errors
|
3
|
+
class FormatError < RuntimeError; end
|
4
|
+
class ConfigurationError < RuntimeError; end
|
5
|
+
class CipherError < RuntimeError; end
|
6
|
+
class TypeMismatch < RuntimeError; end
|
7
|
+
class ExternalError < RuntimeError; end
|
8
|
+
MESSAGES = {
|
9
|
+
encoder: 'No such encoder declared',
|
10
|
+
cipher: 'No such cipher declared',
|
11
|
+
formatter: 'No such formatter declared',
|
12
|
+
no_payload_proto_file: 'No payload .proto file path configured',
|
13
|
+
type_expected: '%s expects %s value, got %s',
|
14
|
+
external_command: 'have issues with system util "%s".
|
15
|
+
Try to run "%s" by hand. Maybe you have to install it'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def err(error_class, message_name, *args, **keywords)
|
19
|
+
message = (Errors::MESSAGES[message_name] || "")
|
20
|
+
message %= args unless args.empty?
|
21
|
+
message %= keywords unless keywords.empty?
|
22
|
+
raise(error_class, message)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Formatters
|
3
|
+
class Default < Base
|
4
|
+
def encode(*args)
|
5
|
+
raise Errors::FormatError if args.size != 2
|
6
|
+
args.map { |part| RbNaCl::Util.bin2hex(part) }
|
7
|
+
.join(Prototok.config[:token_delimiter])
|
8
|
+
end
|
9
|
+
|
10
|
+
def decode(str)
|
11
|
+
parts = str.split(Prototok.config[:token_delimiter])
|
12
|
+
raise Errors::FormatError if parts.size != 2
|
13
|
+
parts.map { |part| RbNaCl::Util.hex2bin(part) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Serializers
|
3
|
+
Autoloaded.class {}
|
4
|
+
extend Utils::Listed
|
5
|
+
|
6
|
+
class Base
|
7
|
+
attr_reader :object
|
8
|
+
|
9
|
+
def initialize(object)
|
10
|
+
@object = object
|
11
|
+
end
|
12
|
+
|
13
|
+
KEY_OPERATIONS = %i[nil empty].freeze
|
14
|
+
|
15
|
+
def encode
|
16
|
+
if attribute_storage.empty?
|
17
|
+
@object.respond_to?(:to_h) ? @object.to_h : @object
|
18
|
+
else
|
19
|
+
Hash[map_attributes]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def attribute_storage
|
26
|
+
self.class.attribute_storage
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_getter(name)
|
30
|
+
@object.respond_to?(name) ? @object.send(name) : @object[name]
|
31
|
+
end
|
32
|
+
|
33
|
+
def map_attributes
|
34
|
+
result = attribute_storage.keys.map {|name| [name, send(name)] }
|
35
|
+
self.class.apply_key_ops!(Hash[result])
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def attribute(*names, **options)
|
40
|
+
names.uniq!
|
41
|
+
update_key_ops(*names, options)
|
42
|
+
names.each do |name|
|
43
|
+
attribute_storage[name] = Attribute.new(options)
|
44
|
+
define_attribute_method name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def decode(data)
|
49
|
+
apply_key_ops!(data)
|
50
|
+
result = attribute_storage.map do |name, attribute|
|
51
|
+
serializer = attribute.serializer
|
52
|
+
key = pick_key data, name
|
53
|
+
next unless key
|
54
|
+
value = data[key]
|
55
|
+
if serializer
|
56
|
+
[name, serializer.decode(value)]
|
57
|
+
else
|
58
|
+
[name, value]
|
59
|
+
end
|
60
|
+
end.compact
|
61
|
+
Hash[result]
|
62
|
+
end
|
63
|
+
|
64
|
+
def attribute_storage
|
65
|
+
@attribute_storage ||= {}
|
66
|
+
end
|
67
|
+
|
68
|
+
def key_ops
|
69
|
+
@key_ops ||= {}
|
70
|
+
end
|
71
|
+
|
72
|
+
def apply_op(result, key, op)
|
73
|
+
if op.is_a? Symbol
|
74
|
+
result.send op, key
|
75
|
+
else
|
76
|
+
op.call(result, key)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def apply_key_ops!(result)
|
81
|
+
key_ops.each do |key, ops|
|
82
|
+
ops.each do |check, op|
|
83
|
+
val = result[key]
|
84
|
+
apply_op(result, key, op) if check_value val, check
|
85
|
+
end
|
86
|
+
end
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
def check_value(val, check)
|
91
|
+
if check.is_a?(Symbol)
|
92
|
+
check_method = "#{check}?"
|
93
|
+
val.respond_to?(check_method) && val.send(check_method)
|
94
|
+
else
|
95
|
+
check.call(val)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
alias_method :attributes, :attribute
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def pick_key(data, key)
|
104
|
+
if data.key?(key)
|
105
|
+
key
|
106
|
+
elsif data.key?(stringified = key.to_s)
|
107
|
+
stringified
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def define_attribute_method(name)
|
112
|
+
if attribute_storage[name].options.empty?
|
113
|
+
define_method(name) { default_getter(name) }
|
114
|
+
else
|
115
|
+
define_getter(name)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def define_getter(name)
|
120
|
+
attribute = attribute_storage[name]
|
121
|
+
define_method name do
|
122
|
+
attribute.serialize default_getter(name)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def update_key_ops(*names, **options)
|
127
|
+
current_key_ops = {}
|
128
|
+
options.each do |k, v|
|
129
|
+
external_option = false
|
130
|
+
raise ArgumentError if v.is_a?(Proc) && v.arity != 2
|
131
|
+
if k.is_a?(Proc)
|
132
|
+
raise ArgumentError if k.arity != 1
|
133
|
+
external_option = true
|
134
|
+
else
|
135
|
+
external_option = KEY_OPERATIONS.include?(k)
|
136
|
+
end
|
137
|
+
current_key_ops[k] = options.delete(k) if external_option
|
138
|
+
end
|
139
|
+
names.each do |name|
|
140
|
+
key_ops[name] = current_key_ops unless current_key_ops.empty?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Prototok
|
4
|
+
CLAIM_ALIASES = [
|
5
|
+
[:exp, %i[expires_at use_before]],
|
6
|
+
[:nbf, %i[not_before use_after]],
|
7
|
+
[:iat, [:created_at]],
|
8
|
+
[:jti, %i[token_id id]],
|
9
|
+
[:payload, []]
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
class Token < Struct.new(*CLAIM_ALIASES.map(&:first))
|
13
|
+
extend Utils::TypeAttributes
|
14
|
+
type ::Time, :exp, :nbf, :iat
|
15
|
+
|
16
|
+
def initialize opts={}
|
17
|
+
super()
|
18
|
+
update!(opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
CLAIM_ALIASES.each do |(original, aliases)|
|
22
|
+
aliases.each do |alias_name|
|
23
|
+
alias_method alias_name, original
|
24
|
+
alias_method "#{alias_name}=", "#{original}="
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def update! opts={}
|
29
|
+
opts.each{|k,v| self.send "#{k}=", v}
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Utils
|
3
|
+
module Listed
|
4
|
+
def find(*attrs)
|
5
|
+
@cache ||= {}
|
6
|
+
@cache[attrs] ||= begin
|
7
|
+
const_name = attrs.map do |word|
|
8
|
+
word.to_s.split(/(?=[[:upper:]])|\_/).map(&:capitalize).join
|
9
|
+
end.join('::')
|
10
|
+
begin
|
11
|
+
const_get const_name, false
|
12
|
+
rescue NameError
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Prototok
|
5
|
+
module Utils
|
6
|
+
module Protoc
|
7
|
+
class << self
|
8
|
+
def cache
|
9
|
+
@cache ||= Set.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def process(path)
|
13
|
+
path = File.expand_path path
|
14
|
+
if !path || !File.exist?(path)
|
15
|
+
raise ArgumentError, 'protobuf proto file is missing'
|
16
|
+
end
|
17
|
+
input = File.read(path)
|
18
|
+
digest = Digest::SHA256.hexdigest(input)
|
19
|
+
if cache.include? digest
|
20
|
+
false
|
21
|
+
else
|
22
|
+
temp = ::Tempfile.new digest
|
23
|
+
temp.write input
|
24
|
+
temp.rewind
|
25
|
+
load_proto temp, digest
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def load_proto(proto, digest)
|
32
|
+
dirname = File.dirname(proto.path)
|
33
|
+
output_rb = proto.path + '_pb.rb'
|
34
|
+
protoc_command = "protoc #{proto.path} --ruby_out=#{dirname} --proto_path=#{dirname}"
|
35
|
+
success = system(protoc_command)
|
36
|
+
Prototok.err(Errors::ExternalError, :external_command, 'protoc', protoc_command) unless success
|
37
|
+
load output_rb
|
38
|
+
cache.add digest
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Utils
|
3
|
+
module TestHelper
|
4
|
+
def option_combinations
|
5
|
+
combinations = (versions + [nil]).map do |v|
|
6
|
+
product_versions(v)
|
7
|
+
end.flatten.each_slice(5).entries
|
8
|
+
combinations.inject({}) do |aggr, options|
|
9
|
+
aggr.merge!(itemize_options(prepare_options(options)))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def prepare_options(o = {})
|
16
|
+
options = { version: o[0], op: o[1], encoder: o[2], formatter: o[3], encoder_options: o[4] }
|
17
|
+
if options[:encoder].to_s == 'protobuf'
|
18
|
+
options[:encoder_options] ||= {}
|
19
|
+
payload_proto_path = File.join(
|
20
|
+
Prototok::Utils::Paths.gem_root,
|
21
|
+
'spec/prototok/encoders/protobuf/test_payload.prot'
|
22
|
+
)
|
23
|
+
options[:encoder_options][:payload_file] = payload_proto_path
|
24
|
+
end
|
25
|
+
options
|
26
|
+
end
|
27
|
+
|
28
|
+
def product_versions(v)
|
29
|
+
[v]
|
30
|
+
.product(cipher_names(v))
|
31
|
+
.product(encoder_names)
|
32
|
+
.product(formatter_names)
|
33
|
+
.product(encoder_options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def itemize_options(keyword_args = {})
|
37
|
+
defaulted = keyword_args.each_with_object({}) do |(key, val), obj|
|
38
|
+
obj[key] = (val.nil? ? 'default' : val)
|
39
|
+
end
|
40
|
+
name = defaulted.to_a.map { |i| i.join(' is ') }.join(', ')
|
41
|
+
{ name => keyword_args.reject { |_k, v| v.nil? } }
|
42
|
+
end
|
43
|
+
|
44
|
+
def encoder_names
|
45
|
+
item_names('lib/prototok/encoders') + [nil]
|
46
|
+
end
|
47
|
+
|
48
|
+
def cipher_names(version = nil)
|
49
|
+
version ||= 1
|
50
|
+
item_names("lib/prototok/ciphers/V#{version}") + [nil]
|
51
|
+
end
|
52
|
+
|
53
|
+
def formatter_names
|
54
|
+
item_names('lib/prototok/formatters') + [nil]
|
55
|
+
end
|
56
|
+
|
57
|
+
def encoder_options
|
58
|
+
[:token, :payload].map do |mode|
|
59
|
+
{encoding_mode: mode}
|
60
|
+
end + [nil]
|
61
|
+
end
|
62
|
+
|
63
|
+
def item_names(subpath)
|
64
|
+
items_root = File.join(Paths.gem_root, subpath)
|
65
|
+
item_files = Dir.entries(items_root).select { |i| i =~ /.+\.rb/ } || []
|
66
|
+
item_files.map { |i| File.basename(i, '.rb') }
|
67
|
+
end
|
68
|
+
|
69
|
+
def versions
|
70
|
+
item_names('lib/prototok/ciphers').map { |i| i.sub(/^v/, '') }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Prototok
|
2
|
+
module Utils
|
3
|
+
module TypeAttributes
|
4
|
+
def type type, *attributes
|
5
|
+
unless type.is_a? Class
|
6
|
+
Prototok.err(Errors::TypeMismatch, :type_expected, :type, Class, type)
|
7
|
+
end
|
8
|
+
attributes.each do |attrib|
|
9
|
+
define_method "#{attrib}=" do |val|
|
10
|
+
unless val.is_a? type
|
11
|
+
Prototok.err(Errors::TypeMismatch, :type_expected, attrib, type, val)
|
12
|
+
end
|
13
|
+
super(val)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/prototok.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'autoloaded'
|
2
|
+
require 'rbnacl'
|
3
|
+
require 'prototok/version'
|
4
|
+
require 'prototok/errors'
|
5
|
+
require 'prototok/utils'
|
6
|
+
require 'prototok/config'
|
7
|
+
require 'prototok/attribute'
|
8
|
+
require 'prototok/serializers'
|
9
|
+
require 'prototok/token'
|
10
|
+
require 'prototok/encoders'
|
11
|
+
require 'prototok/ciphers'
|
12
|
+
require 'prototok/formatters'
|
13
|
+
|
14
|
+
module Prototok
|
15
|
+
class << self
|
16
|
+
def encode(payload=nil, *cipher_args, **opts)
|
17
|
+
raise ArgumentError if payload.nil?
|
18
|
+
header = opts[:header] || {}
|
19
|
+
encoded = encoder_instance(**opts).encode(payload, **header)
|
20
|
+
processed = cipher(**opts).new(*cipher_args).encode(encoded)
|
21
|
+
formatter(opts[:formatter]).new.encode(*processed)
|
22
|
+
end
|
23
|
+
|
24
|
+
def decode(encoded=nil, *cipher_args, **opts)
|
25
|
+
raise ArgumentError if encoded.nil?
|
26
|
+
unformatted = formatter(opts[:formatter]).new.decode(encoded)
|
27
|
+
unprocessed = cipher(**opts).new(*cipher_args).decode(*unformatted)
|
28
|
+
encoder_instance(**opts).decode(unprocessed)
|
29
|
+
end
|
30
|
+
|
31
|
+
def key(*args, **opts)
|
32
|
+
cipher(**opts).key(*args)
|
33
|
+
end
|
34
|
+
|
35
|
+
include Errors
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def encoder_instance(encoder: nil, encoder_options: nil, **_)
|
40
|
+
encoder ||= config[:encoder]
|
41
|
+
klass = Prototok::Encoders.find(encoder) || err(ArgumentError, :encoder)
|
42
|
+
encoder_options ||= {}
|
43
|
+
klass.new(**encoder_options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def cipher(op: nil, version: nil, **_)
|
47
|
+
op ||= config[:op]
|
48
|
+
version ||= config[:version]
|
49
|
+
ver_name = "V#{version}"
|
50
|
+
Prototok::Ciphers.find(ver_name, op) || err(ArgumentError, :cipher)
|
51
|
+
end
|
52
|
+
|
53
|
+
def formatter(frmter_name = nil)
|
54
|
+
frmter_name ||= config[:formatter]
|
55
|
+
Prototok::Formatters.find(frmter_name) || err(ArgumentError, :formatter)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/prototok.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'prototok/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'prototok'
|
9
|
+
spec.version = Prototok::VERSION
|
10
|
+
spec.authors = ['Kostrov Alexander']
|
11
|
+
spec.email = ['bombazook@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'Tokens for sane auth'
|
14
|
+
spec.description = 'Easy to use token generation using libsodium and
|
15
|
+
json (using multi_json), message_pack or protobuf'
|
16
|
+
spec.homepage = 'https://github.com/bombazook/prototok'
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
f.match(%r{^(test|spec|features)/})
|
21
|
+
end
|
22
|
+
spec.bindir = 'exe'
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
spec.add_dependency 'rbnacl'
|
26
|
+
spec.add_dependency 'multi_json'
|
27
|
+
spec.add_dependency 'autoloaded', '~> 2.0'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
30
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
32
|
+
spec.add_development_dependency 'rubocop'
|
33
|
+
spec.add_development_dependency 'rubocop-rspec'
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: prototok
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kostrov Alexander
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rbnacl
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: multi_json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: autoloaded
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.15'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.15'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: |-
|
126
|
+
Easy to use token generation using libsodium and
|
127
|
+
json (using multi_json), message_pack or protobuf
|
128
|
+
email:
|
129
|
+
- bombazook@gmail.com
|
130
|
+
executables: []
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- ".gitignore"
|
135
|
+
- ".rspec"
|
136
|
+
- ".rubocop.yml"
|
137
|
+
- ".travis.yml"
|
138
|
+
- Gemfile
|
139
|
+
- LICENSE.txt
|
140
|
+
- README.md
|
141
|
+
- Rakefile
|
142
|
+
- bin/console
|
143
|
+
- bin/setup
|
144
|
+
- lib/prototok.rb
|
145
|
+
- lib/prototok/attribute.rb
|
146
|
+
- lib/prototok/ciphers.rb
|
147
|
+
- lib/prototok/ciphers/V1/encrypted_mac.rb
|
148
|
+
- lib/prototok/ciphers/V1/encrypted_sign.rb
|
149
|
+
- lib/prototok/ciphers/V1/mac.rb
|
150
|
+
- lib/prototok/ciphers/V1/sign.rb
|
151
|
+
- lib/prototok/ciphers/v1.rb
|
152
|
+
- lib/prototok/config.rb
|
153
|
+
- lib/prototok/encoders.rb
|
154
|
+
- lib/prototok/encoders/json.rb
|
155
|
+
- lib/prototok/encoders/msgpack.rb
|
156
|
+
- lib/prototok/encoders/protobuf.rb
|
157
|
+
- lib/prototok/encoders/protobuf/token.proto
|
158
|
+
- lib/prototok/errors.rb
|
159
|
+
- lib/prototok/formatters.rb
|
160
|
+
- lib/prototok/formatters/default.rb
|
161
|
+
- lib/prototok/serializers.rb
|
162
|
+
- lib/prototok/serializers/time.rb
|
163
|
+
- lib/prototok/serializers/token.rb
|
164
|
+
- lib/prototok/token.rb
|
165
|
+
- lib/prototok/utils.rb
|
166
|
+
- lib/prototok/utils/listed.rb
|
167
|
+
- lib/prototok/utils/paths.rb
|
168
|
+
- lib/prototok/utils/protoc.rb
|
169
|
+
- lib/prototok/utils/test_helper.rb
|
170
|
+
- lib/prototok/utils/type_attributes.rb
|
171
|
+
- lib/prototok/version.rb
|
172
|
+
- prototok.gemspec
|
173
|
+
homepage: https://github.com/bombazook/prototok
|
174
|
+
licenses:
|
175
|
+
- MIT
|
176
|
+
metadata: {}
|
177
|
+
post_install_message:
|
178
|
+
rdoc_options: []
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
requirements: []
|
192
|
+
rubyforge_project:
|
193
|
+
rubygems_version: 2.7.3
|
194
|
+
signing_key:
|
195
|
+
specification_version: 4
|
196
|
+
summary: Tokens for sane auth
|
197
|
+
test_files: []
|