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.
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/