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 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
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /*.gem
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
14
+ .DS_Store
15
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ require: rubocop-rspec
2
+ AllCops:
3
+ RSpec:
4
+ Patterns:
5
+ - 'spec/meander/.+.rb$'
6
+ - 'spec/shared/.+.rb$'
7
+ Metrics/MethodLength:
8
+ Max: 15
9
+ Metrics/BlockLength:
10
+ Exclude:
11
+ - 'spec/**/*.rb'
12
+ - '*.gemspec'
13
+ RSpec/ExampleLength:
14
+ Max: 10
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.15.1
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'google-protobuf'
4
+ gem 'grpc-tools'
5
+ gem 'msgpack'
6
+ gem 'oj'
7
+ gem 'pry'
8
+ gem 'byebug'
9
+ gem 'stackprof'
10
+ # Specify your gem's dependencies in prototok.gemspec
11
+ gemspec
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
@@ -0,0 +1,7 @@
1
+ # Prototok
2
+
3
+ Token generation using RbNacl
4
+
5
+ ## License
6
+
7
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,7 @@
1
+ module Prototok
2
+ module Ciphers
3
+ module V1
4
+ Autoloaded.class {}
5
+ end
6
+ end
7
+ 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,11 @@
1
+ syntax = "proto3";
2
+ import "google/protobuf/any.proto";
3
+ package prototok.protobuf;
4
+
5
+ message Token {
6
+ string exp = 1;
7
+ string nbf = 2;
8
+ string iat = 3;
9
+ string jti = 4;
10
+ google.protobuf.Any payload = 5;
11
+ }
@@ -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,7 @@
1
+ module Prototok
2
+ module Formatters
3
+ Autoloaded.class {}
4
+ extend Utils::Listed
5
+ class Base; end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ require 'time'
2
+
3
+ module Prototok
4
+ module Serializers
5
+ class Time < Base
6
+ def encode
7
+ @object && @object.iso8601(Prototok.config[:time_encoding_precision])
8
+ end
9
+
10
+ def self.decode value
11
+ ::Time.iso8601(value)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ module Prototok
2
+ module Serializers
3
+ class Token < Base
4
+ attributes :exp, :nbf, :iat, serializer: :time, nil: :delete, empty: :delete
5
+ attributes :payload, :jti, nil: :delete
6
+ end
7
+ end
8
+ 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,11 @@
1
+ module Prototok
2
+ module Utils
3
+ module Paths
4
+ class << self
5
+ def gem_root
6
+ Gem::Specification.find_by_name('prototok').gem_dir
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,5 @@
1
+ module Prototok
2
+ module Utils
3
+ Autoloaded.module {}
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Prototok
2
+ VERSION = "0.1.0"
3
+ 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: []