ruby-paseto 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/CHANGELOG.md +8 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +549 -0
- data/lib/paseto/asn1/algorithm_identifier.rb +17 -0
- data/lib/paseto/asn1/curve_private_key.rb +22 -0
- data/lib/paseto/asn1/ec_private_key.rb +27 -0
- data/lib/paseto/asn1/ecdsa_full_r.rb +26 -0
- data/lib/paseto/asn1/ecdsa_sig_value.rb +23 -0
- data/lib/paseto/asn1/ecdsa_signature.rb +49 -0
- data/lib/paseto/asn1/ed25519_identifier.rb +15 -0
- data/lib/paseto/asn1/named_curve.rb +17 -0
- data/lib/paseto/asn1/one_asymmetric_key.rb +32 -0
- data/lib/paseto/asn1/private_key.rb +17 -0
- data/lib/paseto/asn1/private_key_algorithm_identifier.rb +17 -0
- data/lib/paseto/asn1/public_key.rb +17 -0
- data/lib/paseto/asn1/subject_public_key_info.rb +28 -0
- data/lib/paseto/asn1.rb +101 -0
- data/lib/paseto/asymmetric_key.rb +100 -0
- data/lib/paseto/configuration/box.rb +23 -0
- data/lib/paseto/configuration/decode_configuration.rb +68 -0
- data/lib/paseto/configuration.rb +18 -0
- data/lib/paseto/interface/i_d.rb +23 -0
- data/lib/paseto/interface/key.rb +113 -0
- data/lib/paseto/interface/pbkd.rb +83 -0
- data/lib/paseto/interface/pie.rb +59 -0
- data/lib/paseto/interface/pke.rb +86 -0
- data/lib/paseto/interface/serializer.rb +19 -0
- data/lib/paseto/interface/version.rb +161 -0
- data/lib/paseto/interface/wrapper.rb +20 -0
- data/lib/paseto/operations/i_d.rb +48 -0
- data/lib/paseto/operations/id/i_dv3.rb +20 -0
- data/lib/paseto/operations/id/i_dv4.rb +20 -0
- data/lib/paseto/operations/pbkd/p_b_k_dv3.rb +85 -0
- data/lib/paseto/operations/pbkd/p_b_k_dv4.rb +94 -0
- data/lib/paseto/operations/pbkw.rb +73 -0
- data/lib/paseto/operations/pke/p_k_ev3.rb +97 -0
- data/lib/paseto/operations/pke/p_k_ev4.rb +95 -0
- data/lib/paseto/operations/pke.rb +57 -0
- data/lib/paseto/operations/wrap.rb +29 -0
- data/lib/paseto/paserk.rb +55 -0
- data/lib/paseto/paserk_types.rb +46 -0
- data/lib/paseto/protocol/version3.rb +100 -0
- data/lib/paseto/protocol/version4.rb +99 -0
- data/lib/paseto/result.rb +9 -0
- data/lib/paseto/serializer/optional_json.rb +30 -0
- data/lib/paseto/serializer/raw.rb +23 -0
- data/lib/paseto/sodium/curve_25519.rb +46 -0
- data/lib/paseto/sodium/safe_ed25519_loader.rb +19 -0
- data/lib/paseto/sodium/stream/base.rb +82 -0
- data/lib/paseto/sodium/stream/x_cha_cha20_xor.rb +31 -0
- data/lib/paseto/sodium.rb +5 -0
- data/lib/paseto/symmetric_key.rb +119 -0
- data/lib/paseto/token.rb +127 -0
- data/lib/paseto/token_types.rb +29 -0
- data/lib/paseto/util.rb +105 -0
- data/lib/paseto/v3/local.rb +63 -0
- data/lib/paseto/v3/public.rb +204 -0
- data/lib/paseto/v4/local.rb +56 -0
- data/lib/paseto/v4/public.rb +169 -0
- data/lib/paseto/validator.rb +154 -0
- data/lib/paseto/verifiers/footer.rb +30 -0
- data/lib/paseto/verifiers/payload.rb +42 -0
- data/lib/paseto/verify.rb +48 -0
- data/lib/paseto/version.rb +6 -0
- data/lib/paseto/versions.rb +25 -0
- data/lib/paseto/wrappers/pie/pie_v3.rb +72 -0
- data/lib/paseto/wrappers/pie/pie_v4.rb +72 -0
- data/lib/paseto/wrappers/pie.rb +71 -0
- data/lib/paseto.rb +99 -0
- data/paseto.gemspec +58 -0
- data/sorbet/config +3 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
- data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
- data/sorbet/rbi/gems/ffi@1.15.5.rbi +1994 -0
- data/sorbet/rbi/gems/io-console@0.5.11.rbi +8 -0
- data/sorbet/rbi/gems/irb@1.5.1.rbi +342 -0
- data/sorbet/rbi/gems/json@2.6.3.rbi +1541 -0
- data/sorbet/rbi/gems/multi_json@1.15.0.rbi +267 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
- data/sorbet/rbi/gems/oj@3.13.23.rbi +603 -0
- data/sorbet/rbi/gems/openssl@3.0.1.rbi +1735 -0
- data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +407 -0
- data/sorbet/rbi/gems/rake@13.0.6.rbi +3021 -0
- data/sorbet/rbi/gems/rbnacl@7.1.1.rbi +3218 -0
- data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
- data/sorbet/rbi/gems/reline@0.3.1.rbi +8 -0
- data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
- data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10887 -0
- data/sorbet/rbi/gems/rspec-expectations@3.12.0.rbi +8090 -0
- data/sorbet/rbi/gems/rspec-mocks@3.12.0.rbi +5300 -0
- data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
- data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
- data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +219 -0
- data/sorbet/rbi/gems/simplecov@0.21.2.rbi +2135 -0
- data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
- data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
- data/sorbet/rbi/gems/timecop@0.9.6.rbi +350 -0
- data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
- data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
- data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +391 -0
- data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
- data/sorbet/rbi/gems/zeitwerk@2.6.6.rbi +950 -0
- data/sorbet/rbi/shims/multi_json.rbi +19 -0
- data/sorbet/rbi/shims/openssl.rbi +111 -0
- data/sorbet/rbi/shims/rbnacl.rbi +65 -0
- data/sorbet/rbi/shims/zeitwerk.rbi +6 -0
- data/sorbet/rbi/todo.rbi +7 -0
- data/sorbet/tapioca/config.yml +30 -0
- data/sorbet/tapioca/require.rb +12 -0
- metadata +376 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
class Validator
|
|
6
|
+
extend T::Sig
|
|
7
|
+
extend T::Helpers
|
|
8
|
+
|
|
9
|
+
abstract!
|
|
10
|
+
|
|
11
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
12
|
+
attr_reader :payload
|
|
13
|
+
|
|
14
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
15
|
+
attr_reader :options
|
|
16
|
+
|
|
17
|
+
sig { params(payload: T::Hash[String, T.untyped], options: T::Hash[Symbol, T.untyped]).void }
|
|
18
|
+
def initialize(payload, options)
|
|
19
|
+
@payload = payload
|
|
20
|
+
@options = options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { abstract.void }
|
|
24
|
+
def verify; end
|
|
25
|
+
|
|
26
|
+
class Audience < Validator
|
|
27
|
+
sig { override.void }
|
|
28
|
+
def verify
|
|
29
|
+
return unless (aud = options[:verify_aud])
|
|
30
|
+
|
|
31
|
+
given = payload['aud']
|
|
32
|
+
raise InvalidAudience, "Invalid audience. Expected #{aud}, got #{given || '<none>'}" if ([*aud] & [*given]).empty?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Expiration < Validator
|
|
37
|
+
sig { override.void }
|
|
38
|
+
def verify
|
|
39
|
+
return unless options[:verify_exp]
|
|
40
|
+
|
|
41
|
+
given = payload['exp']
|
|
42
|
+
begin
|
|
43
|
+
exp = Time.iso8601(given)
|
|
44
|
+
rescue ArgumentError
|
|
45
|
+
raise ExpiredToken, "Expiry not valid iso8601, got #{given || '<none>'}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
raise ExpiredToken, 'Expiry has passed' if Time.now > exp
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class IssuedAt < Validator
|
|
53
|
+
sig { override.void }
|
|
54
|
+
def verify
|
|
55
|
+
return unless options[:verify_iat]
|
|
56
|
+
|
|
57
|
+
given = payload['iat']
|
|
58
|
+
begin
|
|
59
|
+
iat = Time.iso8601(given)
|
|
60
|
+
rescue ArgumentError
|
|
61
|
+
raise ImmatureToken, "IssuedAt not valid iso8601, got #{given || '<none>'}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
raise ImmatureToken, 'Token is from the future' if Time.now < iat
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Issuer < Validator
|
|
69
|
+
sig { override.void }
|
|
70
|
+
def verify
|
|
71
|
+
return unless (permitted = options[:verify_iss])
|
|
72
|
+
|
|
73
|
+
given = payload['iss']
|
|
74
|
+
permitted = Array(permitted).map { |i| i.is_a?(Symbol) ? i.to_s : i }
|
|
75
|
+
|
|
76
|
+
case given
|
|
77
|
+
when *permitted
|
|
78
|
+
nil
|
|
79
|
+
else
|
|
80
|
+
raise InvalidIssuer, "Invalid issuer. Expected #{permitted}, got #{given || '<none>'}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class WPK < Validator
|
|
86
|
+
PERMITTED = T.let(%w[seal local-wrap secret-wrap].freeze, T::Array[String])
|
|
87
|
+
|
|
88
|
+
sig { override.void }
|
|
89
|
+
def verify
|
|
90
|
+
return unless (wpk = payload['wpk'])
|
|
91
|
+
|
|
92
|
+
wpk.split('.', 3) => [_, String => type, _]
|
|
93
|
+
raise InvalidWPK unless PERMITTED.include?(type)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class KeyID < Validator
|
|
98
|
+
PERMITTED = T.let(%w[lid sid pid].freeze, T::Array[String])
|
|
99
|
+
|
|
100
|
+
sig { override.void }
|
|
101
|
+
def verify
|
|
102
|
+
return unless (kid = payload['kid'])
|
|
103
|
+
|
|
104
|
+
case kid.split('.')
|
|
105
|
+
in [_, String => type, _] if PERMITTED.include?(type)
|
|
106
|
+
nil
|
|
107
|
+
else
|
|
108
|
+
raise InvalidKID
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class NotBefore < Validator
|
|
114
|
+
sig { override.void }
|
|
115
|
+
def verify
|
|
116
|
+
return unless options[:verify_nbf]
|
|
117
|
+
|
|
118
|
+
given = payload['nbf']
|
|
119
|
+
begin
|
|
120
|
+
nbf = Time.iso8601(given)
|
|
121
|
+
rescue ArgumentError
|
|
122
|
+
raise InactiveToken, "NotBefore not valid iso8601, got #{given || '<none>'}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
raise InactiveToken, 'Not yet active' if nbf > Time.now
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class Subject < Validator
|
|
130
|
+
sig { override.void }
|
|
131
|
+
def verify
|
|
132
|
+
return unless (sub = options[:verify_sub])
|
|
133
|
+
|
|
134
|
+
given = payload['sub']
|
|
135
|
+
|
|
136
|
+
raise InvalidSubject, "Invalid subject. Expected #{sub}, got #{given || '<none>'}" unless sub == given
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class TokenIdentifier < Validator
|
|
141
|
+
sig { override.void }
|
|
142
|
+
def verify
|
|
143
|
+
return unless (jti = options[:verify_jti])
|
|
144
|
+
|
|
145
|
+
given = payload['jti']
|
|
146
|
+
if jti.respond_to?(:call)
|
|
147
|
+
raise InvalidTokenIdentifier, 'Invalid jti' unless jti.call(given)
|
|
148
|
+
elsif given.to_s.empty?
|
|
149
|
+
raise InvalidTokenIdentifier, 'Missing jti'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
module Verifiers
|
|
6
|
+
class Footer < T::Enum
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
enums do
|
|
10
|
+
ForbiddenWPKValue = new
|
|
11
|
+
ForbiddenKIDValue = new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sig { params(footer: T::Hash[String, T.untyped], options: T::Hash[Symbol, T.untyped]).void }
|
|
15
|
+
def self.verify(footer, options)
|
|
16
|
+
values.each { |v| v.verifier.new(footer, options).verify }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { returns(T.class_of(Validator)) }
|
|
20
|
+
def verifier
|
|
21
|
+
case self
|
|
22
|
+
when ForbiddenWPKValue then Paseto::Validator::WPK
|
|
23
|
+
when ForbiddenKIDValue then Paseto::Validator::KeyID
|
|
24
|
+
else
|
|
25
|
+
T.absurd(self)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
module Verifiers
|
|
6
|
+
class Payload < T::Enum
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
enums do
|
|
10
|
+
Audience = new
|
|
11
|
+
IssuedAt = new
|
|
12
|
+
Issuer = new
|
|
13
|
+
Expiration = new
|
|
14
|
+
NotBefore = new
|
|
15
|
+
Subject = new
|
|
16
|
+
TokenIdentifier = new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { params(body: T::Hash[String, T.untyped], options: T::Hash[Symbol, T.untyped]).void }
|
|
20
|
+
def self.verify(body, options)
|
|
21
|
+
values.each { |v| v.verifier.new(body, options).verify }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { returns(T.class_of(Validator)) }
|
|
25
|
+
def verifier # rubocop:disable Metrics/CyclomaticComplexity
|
|
26
|
+
case self
|
|
27
|
+
when Audience then Paseto::Validator::Audience
|
|
28
|
+
when IssuedAt then Paseto::Validator::IssuedAt
|
|
29
|
+
when Issuer then Paseto::Validator::Issuer
|
|
30
|
+
when Expiration then Paseto::Validator::Expiration
|
|
31
|
+
when NotBefore then Paseto::Validator::NotBefore
|
|
32
|
+
when Subject then Paseto::Validator::Subject
|
|
33
|
+
when TokenIdentifier then Paseto::Validator::TokenIdentifier
|
|
34
|
+
else
|
|
35
|
+
# :nocov:
|
|
36
|
+
T.absurd(self)
|
|
37
|
+
# :nocov:
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
class Verify
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
sig { returns(Result) }
|
|
9
|
+
attr_reader :result
|
|
10
|
+
|
|
11
|
+
sig do
|
|
12
|
+
params(
|
|
13
|
+
result: Result,
|
|
14
|
+
options: T::Hash[Symbol, T.untyped]
|
|
15
|
+
).returns(Result)
|
|
16
|
+
end
|
|
17
|
+
def self.verify(result, options = {})
|
|
18
|
+
new(result, Paseto.config.decode.to_h.merge(options))
|
|
19
|
+
.then(&:verify_footer)
|
|
20
|
+
.then(&:verify_claims)
|
|
21
|
+
.then(&:result)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig do
|
|
25
|
+
params(
|
|
26
|
+
result: Result,
|
|
27
|
+
options: T::Hash[Symbol, T.untyped]
|
|
28
|
+
).void
|
|
29
|
+
end
|
|
30
|
+
def initialize(result, options)
|
|
31
|
+
@result = result
|
|
32
|
+
@options = options
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { returns(T.self_type) }
|
|
36
|
+
def verify_claims
|
|
37
|
+
Verifiers::Payload.verify(@result.claims, @options)
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { returns(T.self_type) }
|
|
42
|
+
def verify_footer
|
|
43
|
+
footer = @result.footer
|
|
44
|
+
Verifiers::Footer.verify(footer, @options) if footer.is_a?(Hash)
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
class Versions < T::Enum
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
enums do
|
|
9
|
+
V3Version = new(Protocol::Version3)
|
|
10
|
+
V4Version = new(Protocol::Version4)
|
|
11
|
+
V3Str = new('v3')
|
|
12
|
+
V4Str = new('v4')
|
|
13
|
+
K3Str = new('k3')
|
|
14
|
+
K4Str = new('k4')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { returns(Interface::Version) }
|
|
18
|
+
def instance
|
|
19
|
+
case self
|
|
20
|
+
when V3Version, V3Str, K3Str then Protocol::Version3.new
|
|
21
|
+
when V4Version, V4Str, K4Str then Protocol::Version4.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# encoding: binary
|
|
2
|
+
# typed: strict
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Paseto
|
|
6
|
+
module Wrappers
|
|
7
|
+
class PIE
|
|
8
|
+
class PieV3
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
include Interface::PIE
|
|
12
|
+
|
|
13
|
+
DOMAIN_SEPARATOR_AUTH = "\x81"
|
|
14
|
+
DOMAIN_SEPARATOR_ENCRYPT = "\x80"
|
|
15
|
+
|
|
16
|
+
sig { override.params(data: String).returns({ t: String, n: String, c: String }) }
|
|
17
|
+
def self.decode_and_split(data)
|
|
18
|
+
b = Util.decode64(data)
|
|
19
|
+
{
|
|
20
|
+
t: T.must(b.byteslice(0, 48)),
|
|
21
|
+
n: T.must(b.byteslice(48, 32)),
|
|
22
|
+
c: T.must(b.byteslice(80..))
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.returns(Protocol::Version3) }
|
|
27
|
+
def self.protocol
|
|
28
|
+
Protocol::Version3.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { override.returns(String) }
|
|
32
|
+
def local_header
|
|
33
|
+
'k3.local-wrap.pie.'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { override.returns(String) }
|
|
37
|
+
def secret_header
|
|
38
|
+
'k3.secret-wrap.pie.'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(wrapping_key: SymmetricKey).void }
|
|
42
|
+
def initialize(wrapping_key)
|
|
43
|
+
@wrapping_key = wrapping_key
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { override.params(nonce: String).returns(String) }
|
|
47
|
+
def authentication_key(nonce:)
|
|
48
|
+
protocol.hmac("#{DOMAIN_SEPARATOR_AUTH}#{nonce}", key: @wrapping_key.to_bytes, digest_size: 32)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { override.params(payload: String, auth_key: String).returns(String) }
|
|
52
|
+
def authentication_tag(payload:, auth_key:)
|
|
53
|
+
protocol.hmac(payload, key: auth_key)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { override.returns(String) }
|
|
57
|
+
def random_nonce
|
|
58
|
+
protocol.random(32)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { override.params(nonce: String, payload: String).returns(String) }
|
|
62
|
+
def crypt(nonce:, payload:)
|
|
63
|
+
x = OpenSSL::HMAC.digest('SHA384', @wrapping_key.to_bytes, "#{DOMAIN_SEPARATOR_ENCRYPT}#{nonce}")
|
|
64
|
+
ek = T.must(x[0, 32])
|
|
65
|
+
n2 = T.must(x[32..])
|
|
66
|
+
|
|
67
|
+
protocol.crypt(key: ek, nonce: n2, payload: payload)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# encoding: binary
|
|
2
|
+
# typed: strict
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Paseto
|
|
6
|
+
module Wrappers
|
|
7
|
+
class PIE
|
|
8
|
+
class PieV4
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
include Interface::PIE
|
|
12
|
+
|
|
13
|
+
DOMAIN_SEPARATOR_AUTH = "\x81"
|
|
14
|
+
DOMAIN_SEPARATOR_ENCRYPT = "\x80"
|
|
15
|
+
|
|
16
|
+
sig { override.params(data: String).returns({ t: String, n: String, c: String }) }
|
|
17
|
+
def self.decode_and_split(data)
|
|
18
|
+
b = Util.decode64(data)
|
|
19
|
+
{
|
|
20
|
+
t: T.must(b.byteslice(0, 32)),
|
|
21
|
+
n: T.must(b.byteslice(32, 32)),
|
|
22
|
+
c: T.must(b.byteslice(64..))
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.returns(Protocol::Version4) }
|
|
27
|
+
def self.protocol
|
|
28
|
+
Protocol::Version4.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { override.returns(String) }
|
|
32
|
+
def local_header
|
|
33
|
+
'k4.local-wrap.pie.'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { override.returns(String) }
|
|
37
|
+
def secret_header
|
|
38
|
+
'k4.secret-wrap.pie.'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(wrapping_key: SymmetricKey).void }
|
|
42
|
+
def initialize(wrapping_key)
|
|
43
|
+
@wrapping_key = wrapping_key
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { override.params(nonce: String).returns(String) }
|
|
47
|
+
def authentication_key(nonce:)
|
|
48
|
+
protocol.hmac("#{DOMAIN_SEPARATOR_AUTH}#{nonce}", key: @wrapping_key.to_bytes, digest_size: 32)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { override.params(payload: String, auth_key: String).returns(String) }
|
|
52
|
+
def authentication_tag(payload:, auth_key:)
|
|
53
|
+
protocol.hmac(payload, key: auth_key, digest_size: 32)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { override.returns(String) }
|
|
57
|
+
def random_nonce
|
|
58
|
+
protocol.random(32)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { override.params(nonce: String, payload: String).returns(String) }
|
|
62
|
+
def crypt(nonce:, payload:)
|
|
63
|
+
x = protocol.hmac("#{DOMAIN_SEPARATOR_ENCRYPT}#{nonce}", key: @wrapping_key.to_bytes, digest_size: 56)
|
|
64
|
+
ek = T.must(x[0, 32])
|
|
65
|
+
n2 = T.must(x[32..])
|
|
66
|
+
|
|
67
|
+
protocol.crypt(key: ek, nonce: n2, payload: payload)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# encoding: binary
|
|
2
|
+
# typed: strict
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Paseto
|
|
6
|
+
module Wrappers
|
|
7
|
+
class PIE
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
include Interface::Wrapper
|
|
11
|
+
|
|
12
|
+
sig { params(wrapping_key: SymmetricKey).void }
|
|
13
|
+
def initialize(wrapping_key)
|
|
14
|
+
@wrapping_key = wrapping_key
|
|
15
|
+
@coder = T.let(wrapping_key.pie, Interface::PIE)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { override.params(key: Interface::Key, nonce: T.nilable(String)).returns(String) }
|
|
19
|
+
def encode(key, nonce: nil)
|
|
20
|
+
raise LucidityError unless key.version == @wrapping_key.version
|
|
21
|
+
|
|
22
|
+
nonce ||= @coder.random_nonce
|
|
23
|
+
header = pie_header(key)
|
|
24
|
+
|
|
25
|
+
c = @coder.crypt(nonce: nonce, payload: key.to_bytes)
|
|
26
|
+
|
|
27
|
+
ak = @coder.authentication_key(nonce: nonce)
|
|
28
|
+
t = @coder.authentication_tag(payload: "#{header}#{nonce}#{c}", auth_key: ak)
|
|
29
|
+
|
|
30
|
+
[header, Util.encode64("#{t}#{nonce}#{c}")].join
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { override.params(paserk: [String, String, String, String]).returns(Interface::Key) }
|
|
34
|
+
def decode(paserk)
|
|
35
|
+
paserk => [version, type, protocol, data]
|
|
36
|
+
raise UnknownProtocol, 'payload does not use PIE' unless protocol == 'pie'
|
|
37
|
+
raise ParseError, 'not a valid PIE PASERK' if data.empty?
|
|
38
|
+
raise LucidityError unless version == @wrapping_key.paserk_version
|
|
39
|
+
|
|
40
|
+
header = "#{version}.#{type}.pie."
|
|
41
|
+
|
|
42
|
+
# :nocov:
|
|
43
|
+
@coder.decode_and_split(data) => {t:, n:, c:}
|
|
44
|
+
# :nocov:
|
|
45
|
+
|
|
46
|
+
ak = @coder.authentication_key(nonce: n)
|
|
47
|
+
t2 = @coder.authentication_tag(payload: "#{header}#{n}#{c}", auth_key: ak)
|
|
48
|
+
|
|
49
|
+
raise InvalidAuthenticator unless Util.constant_compare(t, t2)
|
|
50
|
+
|
|
51
|
+
ptk = @coder.crypt(nonce: n, payload: c)
|
|
52
|
+
|
|
53
|
+
PaserkTypes.deserialize("#{version}.#{type}").generate(ptk)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
sig { params(key: Interface::Key).returns(String) }
|
|
59
|
+
def pie_header(key)
|
|
60
|
+
case key
|
|
61
|
+
when SymmetricKey then @coder.local_header
|
|
62
|
+
when AsymmetricKey then @coder.secret_header
|
|
63
|
+
else
|
|
64
|
+
# :nocov:
|
|
65
|
+
raise ArgumentError, 'not a valid type of key'
|
|
66
|
+
# :nocov:
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/paseto.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# encoding: binary
|
|
2
|
+
# typed: strict
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'base64'
|
|
6
|
+
require 'multi_json'
|
|
7
|
+
require 'openssl'
|
|
8
|
+
begin
|
|
9
|
+
require 'rbnacl'
|
|
10
|
+
rescue LoadError
|
|
11
|
+
raise if defined?(RbNaCl)
|
|
12
|
+
end
|
|
13
|
+
require 'securerandom'
|
|
14
|
+
require 'sorbet-runtime'
|
|
15
|
+
T::Configuration.enable_final_checks_on_hooks
|
|
16
|
+
require 'time'
|
|
17
|
+
require 'zeitwerk'
|
|
18
|
+
|
|
19
|
+
loader = Zeitwerk::Loader.for_gem
|
|
20
|
+
unless defined?(RbNaCl)
|
|
21
|
+
loader.ignore(
|
|
22
|
+
"#{__dir__}/paseto/v4/",
|
|
23
|
+
"#{__dir__}/paseto/sodium/",
|
|
24
|
+
"#{__dir__}/paseto/sodium.rb"
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
loader.inflector.inflect(
|
|
28
|
+
'asn1' => 'ASN1',
|
|
29
|
+
'ec_private_key' => 'ECPrivateKey',
|
|
30
|
+
'ecdsa_sig_value' => 'ECDSASigValue',
|
|
31
|
+
'ecdsa_signature' => 'ECDSASignature',
|
|
32
|
+
'ecdsa_full_r' => 'ECDSAFullR',
|
|
33
|
+
'pie' => 'PIE',
|
|
34
|
+
'pbkw' => 'PBKW',
|
|
35
|
+
'pbkd' => 'PBKD',
|
|
36
|
+
'id' => 'ID',
|
|
37
|
+
'pke' => 'PKE'
|
|
38
|
+
)
|
|
39
|
+
loader.setup
|
|
40
|
+
|
|
41
|
+
module Paseto
|
|
42
|
+
extend T::Sig
|
|
43
|
+
extend Configuration
|
|
44
|
+
|
|
45
|
+
class Error < StandardError; end
|
|
46
|
+
|
|
47
|
+
class AlgorithmError < Error; end
|
|
48
|
+
# An algorithm assertion was violated, such as attempting to wrap a
|
|
49
|
+
# v4 key with a v3 key, or providing an EC key other than secp384 to v3.
|
|
50
|
+
class LucidityError < AlgorithmError; end
|
|
51
|
+
|
|
52
|
+
# A cryptographic primitive has failed for any reason,
|
|
53
|
+
# such as attempting to initialize a stream cipher with
|
|
54
|
+
# an invalid nonce.
|
|
55
|
+
class CryptoError < Error; end
|
|
56
|
+
# An authenticator was forged or otherwise corrupt
|
|
57
|
+
class InvalidAuthenticator < CryptoError; end
|
|
58
|
+
# A signature was forged or otherwise corrupt
|
|
59
|
+
class InvalidSignature < CryptoError; end
|
|
60
|
+
# Key is not valid for algorithm
|
|
61
|
+
class InvalidKeyPair < CryptoError; end
|
|
62
|
+
|
|
63
|
+
# Superclass for claim validation errors
|
|
64
|
+
class ValidationError < Error; end
|
|
65
|
+
# Token is expired
|
|
66
|
+
class ExpiredToken < ValidationError; end
|
|
67
|
+
# Token has a nbf before the current time
|
|
68
|
+
class InactiveToken < ValidationError; end
|
|
69
|
+
# Disallowed issuer
|
|
70
|
+
class InvalidIssuer < ValidationError; end
|
|
71
|
+
# Incorrect audience
|
|
72
|
+
class InvalidAudience < ValidationError; end
|
|
73
|
+
# Token issued in the future
|
|
74
|
+
class ImmatureToken < ValidationError; end
|
|
75
|
+
# Unacceptable sub
|
|
76
|
+
class InvalidSubject < ValidationError; end
|
|
77
|
+
# Missing or unacceptable jti
|
|
78
|
+
class InvalidTokenIdentifier < ValidationError; end
|
|
79
|
+
# Footer contains unknown or forbidden payload types
|
|
80
|
+
class InvalidWPK < ValidationError; end
|
|
81
|
+
# Footer contains unknown or invalid KID payloads
|
|
82
|
+
class InvalidKID < ValidationError; end
|
|
83
|
+
|
|
84
|
+
class PaserkError < Error; end
|
|
85
|
+
class UnknownProtocol < PaserkError; end
|
|
86
|
+
class UnknownOperation < PaserkError; end
|
|
87
|
+
|
|
88
|
+
# Deserialized data did not include mandatory fields.
|
|
89
|
+
class ParseError < Error; end
|
|
90
|
+
# Tried to work with a V4 token without RbNaCl loaded
|
|
91
|
+
class UnsupportedToken < ParseError; end
|
|
92
|
+
|
|
93
|
+
sig { returns(T::Boolean) }
|
|
94
|
+
def self.rbnacl?
|
|
95
|
+
!!defined?(RbNaCl)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
loader.eager_load
|
data/paseto.gemspec
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/paseto/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'ruby-paseto'
|
|
7
|
+
spec.version = Paseto::VERSION
|
|
8
|
+
spec.platform = Gem::Platform::RUBY
|
|
9
|
+
spec.authors = ['Joe Truba']
|
|
10
|
+
spec.email = ['joe@bannable.net']
|
|
11
|
+
|
|
12
|
+
spec.summary = 'A ruby implementation of PASETO and PASERK tokens'
|
|
13
|
+
spec.description = <<-DESCRIPTION
|
|
14
|
+
Platform Agnostic SEcurity TOkens are a specification for secure stateless tokens.
|
|
15
|
+
This is an implementation of PASETO tokens, and the PASERK key management extensions,
|
|
16
|
+
in ruby, with runtime static type checking provided by Sorbet.
|
|
17
|
+
DESCRIPTION
|
|
18
|
+
spec.homepage = 'https://github.com/bannable/paseto'
|
|
19
|
+
spec.license = 'MIT'
|
|
20
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
21
|
+
|
|
22
|
+
spec.metadata = {
|
|
23
|
+
'bug_tracker_uri' => 'https://github.com/bannable/paseto/issues',
|
|
24
|
+
'changelog_uri' => 'https://github.com/bannable/paseto/blob/main/CHANGELOG.md',
|
|
25
|
+
'documentation_uri' => 'https://github.com/bannable/paseto',
|
|
26
|
+
'homepage_uri' => 'https://github.com/bannable/paseto',
|
|
27
|
+
'source_code_uri' => 'https://github.com/bannable/paseto',
|
|
28
|
+
'rubygems_mfa_required' => 'true'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
32
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
33
|
+
f.match(/^(?:bin|spec|coverage|tmp|devcontainers|gemfiles)/) || # Irrelevant directories
|
|
34
|
+
f.match(/^\.+/) || # Anything starting with .
|
|
35
|
+
f.match(/^(Gemfile|Gemfile\.lock|Rakefile|Appraisals)$/) # Irrelevant files
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
spec.require_paths = ['lib']
|
|
39
|
+
|
|
40
|
+
spec.add_runtime_dependency 'multi_json', '~> 1.15.0'
|
|
41
|
+
spec.add_runtime_dependency 'openssl', '~> 3.0.0'
|
|
42
|
+
spec.add_runtime_dependency 'sorbet-runtime'
|
|
43
|
+
spec.add_runtime_dependency 'zeitwerk'
|
|
44
|
+
|
|
45
|
+
spec.add_development_dependency 'appraisal'
|
|
46
|
+
spec.add_development_dependency 'bundler', '~> 2'
|
|
47
|
+
spec.add_development_dependency 'rake'
|
|
48
|
+
spec.add_development_dependency 'rspec'
|
|
49
|
+
spec.add_development_dependency 'rubocop', '~> 1.38.0'
|
|
50
|
+
spec.add_development_dependency 'rubocop-performance', '~> 1.15.0'
|
|
51
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 2.14.2'
|
|
52
|
+
spec.add_development_dependency 'rubocop-sorbet', '~> 0.6.11'
|
|
53
|
+
spec.add_development_dependency 'simplecov'
|
|
54
|
+
spec.add_development_dependency 'sorbet'
|
|
55
|
+
spec.add_development_dependency 'timecop'
|
|
56
|
+
|
|
57
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
58
|
+
end
|
data/sorbet/config
ADDED