ruby-paseto 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +549 -0
  6. data/lib/paseto/asn1/algorithm_identifier.rb +17 -0
  7. data/lib/paseto/asn1/curve_private_key.rb +22 -0
  8. data/lib/paseto/asn1/ec_private_key.rb +27 -0
  9. data/lib/paseto/asn1/ecdsa_full_r.rb +26 -0
  10. data/lib/paseto/asn1/ecdsa_sig_value.rb +23 -0
  11. data/lib/paseto/asn1/ecdsa_signature.rb +49 -0
  12. data/lib/paseto/asn1/ed25519_identifier.rb +15 -0
  13. data/lib/paseto/asn1/named_curve.rb +17 -0
  14. data/lib/paseto/asn1/one_asymmetric_key.rb +32 -0
  15. data/lib/paseto/asn1/private_key.rb +17 -0
  16. data/lib/paseto/asn1/private_key_algorithm_identifier.rb +17 -0
  17. data/lib/paseto/asn1/public_key.rb +17 -0
  18. data/lib/paseto/asn1/subject_public_key_info.rb +28 -0
  19. data/lib/paseto/asn1.rb +101 -0
  20. data/lib/paseto/asymmetric_key.rb +100 -0
  21. data/lib/paseto/configuration/box.rb +23 -0
  22. data/lib/paseto/configuration/decode_configuration.rb +68 -0
  23. data/lib/paseto/configuration.rb +18 -0
  24. data/lib/paseto/interface/i_d.rb +23 -0
  25. data/lib/paseto/interface/key.rb +113 -0
  26. data/lib/paseto/interface/pbkd.rb +83 -0
  27. data/lib/paseto/interface/pie.rb +59 -0
  28. data/lib/paseto/interface/pke.rb +86 -0
  29. data/lib/paseto/interface/serializer.rb +19 -0
  30. data/lib/paseto/interface/version.rb +161 -0
  31. data/lib/paseto/interface/wrapper.rb +20 -0
  32. data/lib/paseto/operations/i_d.rb +48 -0
  33. data/lib/paseto/operations/id/i_dv3.rb +20 -0
  34. data/lib/paseto/operations/id/i_dv4.rb +20 -0
  35. data/lib/paseto/operations/pbkd/p_b_k_dv3.rb +85 -0
  36. data/lib/paseto/operations/pbkd/p_b_k_dv4.rb +94 -0
  37. data/lib/paseto/operations/pbkw.rb +73 -0
  38. data/lib/paseto/operations/pke/p_k_ev3.rb +97 -0
  39. data/lib/paseto/operations/pke/p_k_ev4.rb +95 -0
  40. data/lib/paseto/operations/pke.rb +57 -0
  41. data/lib/paseto/operations/wrap.rb +29 -0
  42. data/lib/paseto/paserk.rb +55 -0
  43. data/lib/paseto/paserk_types.rb +46 -0
  44. data/lib/paseto/protocol/version3.rb +100 -0
  45. data/lib/paseto/protocol/version4.rb +99 -0
  46. data/lib/paseto/result.rb +9 -0
  47. data/lib/paseto/serializer/optional_json.rb +30 -0
  48. data/lib/paseto/serializer/raw.rb +23 -0
  49. data/lib/paseto/sodium/curve_25519.rb +46 -0
  50. data/lib/paseto/sodium/safe_ed25519_loader.rb +19 -0
  51. data/lib/paseto/sodium/stream/base.rb +82 -0
  52. data/lib/paseto/sodium/stream/x_cha_cha20_xor.rb +31 -0
  53. data/lib/paseto/sodium.rb +5 -0
  54. data/lib/paseto/symmetric_key.rb +119 -0
  55. data/lib/paseto/token.rb +127 -0
  56. data/lib/paseto/token_types.rb +29 -0
  57. data/lib/paseto/util.rb +105 -0
  58. data/lib/paseto/v3/local.rb +63 -0
  59. data/lib/paseto/v3/public.rb +204 -0
  60. data/lib/paseto/v4/local.rb +56 -0
  61. data/lib/paseto/v4/public.rb +169 -0
  62. data/lib/paseto/validator.rb +154 -0
  63. data/lib/paseto/verifiers/footer.rb +30 -0
  64. data/lib/paseto/verifiers/payload.rb +42 -0
  65. data/lib/paseto/verify.rb +48 -0
  66. data/lib/paseto/version.rb +6 -0
  67. data/lib/paseto/versions.rb +25 -0
  68. data/lib/paseto/wrappers/pie/pie_v3.rb +72 -0
  69. data/lib/paseto/wrappers/pie/pie_v4.rb +72 -0
  70. data/lib/paseto/wrappers/pie.rb +71 -0
  71. data/lib/paseto.rb +99 -0
  72. data/paseto.gemspec +58 -0
  73. data/sorbet/config +3 -0
  74. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  75. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  76. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
  77. data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
  78. data/sorbet/rbi/gems/ffi@1.15.5.rbi +1994 -0
  79. data/sorbet/rbi/gems/io-console@0.5.11.rbi +8 -0
  80. data/sorbet/rbi/gems/irb@1.5.1.rbi +342 -0
  81. data/sorbet/rbi/gems/json@2.6.3.rbi +1541 -0
  82. data/sorbet/rbi/gems/multi_json@1.15.0.rbi +267 -0
  83. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  84. data/sorbet/rbi/gems/oj@3.13.23.rbi +603 -0
  85. data/sorbet/rbi/gems/openssl@3.0.1.rbi +1735 -0
  86. data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
  87. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +407 -0
  88. data/sorbet/rbi/gems/rake@13.0.6.rbi +3021 -0
  89. data/sorbet/rbi/gems/rbnacl@7.1.1.rbi +3218 -0
  90. data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
  91. data/sorbet/rbi/gems/reline@0.3.1.rbi +8 -0
  92. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  93. data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10887 -0
  94. data/sorbet/rbi/gems/rspec-expectations@3.12.0.rbi +8090 -0
  95. data/sorbet/rbi/gems/rspec-mocks@3.12.0.rbi +5300 -0
  96. data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
  97. data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
  98. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
  99. data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +219 -0
  100. data/sorbet/rbi/gems/simplecov@0.21.2.rbi +2135 -0
  101. data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
  102. data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
  103. data/sorbet/rbi/gems/timecop@0.9.6.rbi +350 -0
  104. data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
  105. data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
  106. data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +391 -0
  107. data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
  108. data/sorbet/rbi/gems/zeitwerk@2.6.6.rbi +950 -0
  109. data/sorbet/rbi/shims/multi_json.rbi +19 -0
  110. data/sorbet/rbi/shims/openssl.rbi +111 -0
  111. data/sorbet/rbi/shims/rbnacl.rbi +65 -0
  112. data/sorbet/rbi/shims/zeitwerk.rbi +6 -0
  113. data/sorbet/rbi/todo.rbi +7 -0
  114. data/sorbet/tapioca/config.yml +30 -0
  115. data/sorbet/tapioca/require.rb +12 -0
  116. 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,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Paseto
5
+ VERSION = '0.1.0'
6
+ 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
@@ -0,0 +1,3 @@
1
+ --dir
2
+ .
3
+ --ignore=vendor/