ruby-paseto 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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