Skip to content

Commit d3ed671

Browse files
Remove base64 dependency (#459)
* refactor: extract encoding logic into its own classes * refactor: move logic for loading the correct encoder to `Encoders` module * refactor: move `WebAuthn.standard_encoder` to `lib/webauthn/encoders.rb` * refactor: remove default value from `Encoders.lookup` `encoding` argument Let's add it to `Encoders.new` for backward compatibility. * fix: completely remove `base64` dependency It is not a runtime dependency yet we were still using it in `lib/webauthn/u2f_migrator.rb`.
1 parent cb54510 commit d3ed671

11 files changed

+131
-99
lines changed

lib/webauthn/encoder.rb

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,18 @@
11
# frozen_string_literal: true
22

3-
require "base64"
3+
require "webauthn/encoders"
44

55
module WebAuthn
6-
def self.standard_encoder
7-
@standard_encoder ||= Encoder.new
8-
end
9-
106
class Encoder
7+
extend Forwardable
8+
119
# https://www.w3.org/TR/webauthn-2/#base64url-encoding
1210
STANDARD_ENCODING = :base64url
1311

14-
attr_reader :encoding
12+
def_delegators :@encoder_klass, :encode, :decode
1513

1614
def initialize(encoding = STANDARD_ENCODING)
17-
@encoding = encoding
18-
end
19-
20-
def encode(data)
21-
case encoding
22-
when :base64
23-
[data].pack("m0") # Base64.strict_encode64(data)
24-
when :base64url
25-
data = [data].pack("m0") # Base64.urlsafe_encode64(data, padding: false)
26-
data.chomp!("==") or data.chomp!("=")
27-
data.tr!("+/", "-_")
28-
data
29-
when nil, false
30-
data
31-
else
32-
raise "Unsupported or unknown encoding: #{encoding}"
33-
end
34-
end
35-
36-
def decode(data)
37-
case encoding
38-
when :base64
39-
data.unpack1("m0") # Base64.strict_decode64(data)
40-
when :base64url
41-
if !data.end_with?("=") && data.length % 4 != 0 # Base64.urlsafe_decode64(data)
42-
data = data.ljust((data.length + 3) & ~3, "=")
43-
data.tr!("-_", "+/")
44-
else
45-
data = data.tr("-_", "+/")
46-
end
47-
data.unpack1("m0")
48-
when nil, false
49-
data
50-
else
51-
raise "Unsupported or unknown encoding: #{encoding}"
52-
end
15+
@encoder_klass = Encoders.lookup(encoding)
5316
end
5417
end
5518
end

lib/webauthn/encoders.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module WebAuthn
4+
def self.standard_encoder
5+
@standard_encoder ||= Encoders.lookup(Encoder::STANDARD_ENCODING)
6+
end
7+
8+
module Encoders
9+
class << self
10+
def lookup(encoding)
11+
case encoding
12+
when :base64
13+
Base64Encoder
14+
when :base64url
15+
Base64UrlEncoder
16+
when nil, false
17+
NullEncoder
18+
else
19+
raise "Unsupported or unknown encoding: #{encoding}"
20+
end
21+
end
22+
end
23+
24+
class Base64Encoder
25+
def self.encode(data)
26+
[data].pack("m0") # Base64.strict_encode64(data)
27+
end
28+
29+
def self.decode(data)
30+
data.unpack1("m0") # Base64.strict_decode64(data)
31+
end
32+
end
33+
34+
class Base64UrlEncoder
35+
def self.encode(data)
36+
data = [data].pack("m0") # Base64.urlsafe_encode64(data, padding: false)
37+
data.chomp!("==") or data.chomp!("=")
38+
data.tr!("+/", "-_")
39+
data
40+
end
41+
42+
def self.decode(data)
43+
if !data.end_with?("=") && data.length % 4 != 0 # Base64.urlsafe_decode64(data)
44+
data = data.ljust((data.length + 3) & ~3, "=")
45+
end
46+
47+
data = data.tr("-_", "+/")
48+
data.unpack1("m0")
49+
end
50+
end
51+
52+
class NullEncoder
53+
def self.encode(data)
54+
data
55+
end
56+
57+
def self.decode(data)
58+
data
59+
end
60+
end
61+
end
62+
end

lib/webauthn/u2f_migrator.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,24 @@ def attestation_type
4343
end
4444

4545
def attestation_trust_path
46-
@attestation_trust_path ||= [OpenSSL::X509::Certificate.new(Base64.strict_decode64(@certificate))]
46+
@attestation_trust_path ||= [
47+
OpenSSL::X509::Certificate.new(WebAuthn::Encoders::Base64Encoder.decode(@certificate))
48+
]
4749
end
4850

4951
private
5052

5153
# https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#u2f-authenticatorMakeCredential-interoperability
5254
# Let credentialId be a credentialIdLength byte array initialized with CTAP1/U2F response key handle bytes.
5355
def credential_id
54-
Base64.urlsafe_decode64(@key_handle)
56+
WebAuthn::Encoders::Base64UrlEncoder.decode(@key_handle)
5557
end
5658

5759
# Let x9encodedUserPublicKey be the user public key returned in the U2F registration response message [U2FRawMsgs].
5860
# Let coseEncodedCredentialPublicKey be the result of converting x9encodedUserPublicKey’s value from ANS X9.62 /
5961
# Sec-1 v2 uncompressed curve point representation [SEC1V2] to COSE_Key representation ([RFC8152] Section 7).
6062
def credential_cose_key
61-
decoded_public_key = Base64.strict_decode64(@public_key)
63+
decoded_public_key = WebAuthn::Encoders::Base64Encoder.decode(@public_key)
6264
if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(decoded_public_key)
6365
COSE::Key::EC2.new(
6466
alg: COSE::Algorithm.by_name("ES256").id,

spec/webauthn/attestation_statement/android_safetynet_spec.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
require "spec_helper"
44

5-
require "base64"
65
require "jwt"
76
require "openssl"
87
require "webauthn/attestation_statement/android_safetynet"
@@ -17,7 +16,7 @@
1716
payload,
1817
attestation_key,
1918
"RS256",
20-
x5c: [Base64.strict_encode64(leaf_certificate.to_der)]
19+
x5c: [WebAuthn::Encoders::Base64Encoder.encode(leaf_certificate.to_der)]
2120
)
2221
end
2322

@@ -26,7 +25,11 @@
2625
end
2726
let(:timestamp) { Time.now }
2827
let(:cts_profile_match) { true }
29-
let(:nonce) { Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(authenticator_data_bytes + client_data_hash)) }
28+
let(:nonce) do
29+
WebAuthn::Encoders::Base64Encoder.encode(
30+
OpenSSL::Digest::SHA256.digest(authenticator_data_bytes + client_data_hash)
31+
)
32+
end
3033
let(:attestation_key) { create_rsa_key }
3134

3235
let(:leaf_certificate) do
@@ -63,7 +66,7 @@
6366
end
6467

6568
context "when nonce is not set to the base64 of the SHA256 of authData + clientDataHash" do
66-
let(:nonce) { Base64.strict_encode64(OpenSSL::Digest.digest("SHA256", "something else")) }
69+
let(:nonce) { WebAuthn::Encoders::Base64Encoder.encode(OpenSSL::Digest.digest("SHA256", "something else")) }
6770

6871
it "returns false" do
6972
expect(statement.valid?(authenticator_data, client_data_hash)).to be_falsy

spec/webauthn/authenticator_assertion_response_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -518,12 +518,12 @@
518518
let(:assertion_data) { seeds[:u2f_migration][:assertion] }
519519
let(:assertion_response) do
520520
WebAuthn::AuthenticatorAssertionResponse.new(
521-
client_data_json: Base64.strict_decode64(assertion_data[:response][:client_data_json]),
522-
authenticator_data: Base64.strict_decode64(assertion_data[:response][:authenticator_data]),
523-
signature: Base64.strict_decode64(assertion_data[:response][:signature])
521+
client_data_json: WebAuthn::Encoders::Base64Encoder.decode(assertion_data[:response][:client_data_json]),
522+
authenticator_data: WebAuthn::Encoders::Base64Encoder.decode(assertion_data[:response][:authenticator_data]),
523+
signature: WebAuthn::Encoders::Base64Encoder.decode(assertion_data[:response][:signature])
524524
)
525525
end
526-
let(:original_challenge) { Base64.strict_decode64(assertion_data[:challenge]) }
526+
let(:original_challenge) { WebAuthn::Encoders::Base64Encoder.decode(assertion_data[:challenge]) }
527527

528528
context "when correct FIDO AppID is given as rp_id" do
529529
it "verifies" do

spec/webauthn/authenticator_attestation_response_spec.rb

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
require "spec_helper"
44
require "support/seeds"
55

6-
require "base64"
76
require "webauthn/authenticator_attestation_response"
87
require "openssl"
98

@@ -114,7 +113,7 @@
114113

115114
context "when fido-u2f attestation" do
116115
let(:original_challenge) do
117-
Base64.strict_decode64(seeds[:security_key_direct][:credential_creation_options][:challenge])
116+
WebAuthn::Encoders::Base64Encoder.decode(seeds[:security_key_direct][:credential_creation_options][:challenge])
118117
end
119118

120119
context "when there is a single origin" do
@@ -124,8 +123,8 @@
124123
response = seeds[:security_key_direct][:authenticator_attestation_response]
125124

126125
WebAuthn::AuthenticatorAttestationResponse.new(
127-
attestation_object: Base64.strict_decode64(response[:attestation_object]),
128-
client_data_json: Base64.strict_decode64(response[:client_data_json])
126+
attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]),
127+
client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json])
129128
)
130129
end
131130

@@ -194,7 +193,7 @@
194193
let(:origin) { "https://localhost:13010" }
195194

196195
let(:original_challenge) do
197-
Base64.strict_decode64(
196+
WebAuthn::Encoders::Base64Encoder.decode(
198197
seeds[:security_key_packed_self][:credential_creation_options][:challenge]
199198
)
200199
end
@@ -203,8 +202,8 @@
203202
response = seeds[:security_key_packed_self][:authenticator_attestation_response]
204203

205204
WebAuthn::AuthenticatorAttestationResponse.new(
206-
attestation_object: Base64.strict_decode64(response[:attestation_object]),
207-
client_data_json: Base64.strict_decode64(response[:client_data_json])
205+
attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]),
206+
client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json])
208207
)
209208
end
210209

@@ -234,7 +233,7 @@
234233
let(:origin) { "http://localhost:3000" }
235234

236235
let(:original_challenge) do
237-
Base64.strict_decode64(
236+
WebAuthn::Encoders::Base64Encoder.decode(
238237
seeds[:security_key_packed_x5c][:credential_creation_options][:challenge]
239238
)
240239
end
@@ -243,8 +242,8 @@
243242
response = seeds[:security_key_packed_x5c][:authenticator_attestation_response]
244243

245244
WebAuthn::AuthenticatorAttestationResponse.new(
246-
attestation_object: Base64.strict_decode64(response[:attestation_object]),
247-
client_data_json: Base64.strict_decode64(response[:client_data_json])
245+
attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]),
246+
client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json])
248247
)
249248
end
250249

@@ -274,14 +273,16 @@
274273
context "when TPM attestation" do
275274
let(:origin) { seeds[:tpm][:origin] }
276275
let(:time) { Time.utc(2019, 8, 13, 22, 6) }
277-
let(:challenge) { Base64.urlsafe_decode64(seeds[:tpm][:credential_creation_options][:challenge]) }
276+
let(:challenge) do
277+
WebAuthn::Encoders::Base64UrlEncoder.decode(seeds[:tpm][:credential_creation_options][:challenge])
278+
end
278279

279280
let(:attestation_response) do
280281
response = seeds[:tpm][:authenticator_attestation_response]
281282

282283
WebAuthn::AuthenticatorAttestationResponse.new(
283-
attestation_object: Base64.urlsafe_decode64(response[:attestation_object]),
284-
client_data_json: Base64.urlsafe_decode64(response[:client_data_json])
284+
attestation_object: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:attestation_object]),
285+
client_data_json: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:client_data_json])
285286
)
286287
end
287288

@@ -334,15 +335,17 @@
334335
let(:origin) { "https://7f41ac45.ngrok.io" }
335336

336337
let(:original_challenge) do
337-
Base64.strict_decode64(seeds[:android_safetynet_direct][:credential_creation_options][:challenge])
338+
WebAuthn::Encoders::Base64Encoder.decode(
339+
seeds[:android_safetynet_direct][:credential_creation_options][:challenge]
340+
)
338341
end
339342

340343
let(:attestation_response) do
341344
response = seeds[:android_safetynet_direct][:authenticator_attestation_response]
342345

343346
WebAuthn::AuthenticatorAttestationResponse.new(
344-
attestation_object: Base64.strict_decode64(response[:attestation_object]),
345-
client_data_json: Base64.strict_decode64(response[:client_data_json])
347+
attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]),
348+
client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json])
346349
)
347350
end
348351

@@ -371,15 +374,15 @@
371374

372375
context "when android-key attestation" do
373376
let(:original_challenge) do
374-
Base64.urlsafe_decode64(seeds[:android_key_direct][:credential_creation_options][:challenge])
377+
WebAuthn::Encoders::Base64UrlEncoder.decode(seeds[:android_key_direct][:credential_creation_options][:challenge])
375378
end
376379

377380
let(:attestation_response) do
378381
response = seeds[:android_key_direct][:authenticator_attestation_response]
379382

380383
WebAuthn::AuthenticatorAttestationResponse.new(
381-
attestation_object: Base64.urlsafe_decode64(response[:attestation_object]),
382-
client_data_json: Base64.urlsafe_decode64(response[:client_data_json])
384+
attestation_object: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:attestation_object]),
385+
client_data_json: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:client_data_json])
383386
)
384387
end
385388

@@ -468,15 +471,15 @@
468471
let(:origin) { seeds[:macbook_touch_id][:origin] }
469472

470473
let(:original_challenge) do
471-
Base64.urlsafe_decode64(seeds[:macbook_touch_id][:credential_creation_options][:challenge])
474+
WebAuthn::Encoders::Base64UrlEncoder.decode(seeds[:macbook_touch_id][:credential_creation_options][:challenge])
472475
end
473476

474477
let(:attestation_response) do
475478
response = seeds[:macbook_touch_id][:authenticator_attestation_response]
476479

477480
WebAuthn::AuthenticatorAttestationResponse.new(
478-
attestation_object: Base64.urlsafe_decode64(response[:attestation_object]),
479-
client_data_json: Base64.urlsafe_decode64(response[:client_data_json])
481+
attestation_object: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:attestation_object]),
482+
client_data_json: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:client_data_json])
480483
)
481484
end
482485

@@ -766,7 +769,7 @@
766769

767770
describe "attestation statement verification" do
768771
let(:original_challenge) do
769-
Base64.strict_decode64(seeds[:security_key_direct][:credential_creation_options][:challenge])
772+
WebAuthn::Encoders::Base64Encoder.decode(seeds[:security_key_direct][:credential_creation_options][:challenge])
770773
end
771774

772775
let(:origin) { "http://localhost:3000" }
@@ -775,8 +778,8 @@
775778
response = seeds[:security_key_direct][:authenticator_attestation_response]
776779

777780
WebAuthn::AuthenticatorAttestationResponse.new(
778-
attestation_object: Base64.strict_decode64(response[:attestation_object]),
779-
client_data_json: Base64.strict_decode64(response[:client_data_json])
781+
attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]),
782+
client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json])
780783
)
781784
end
782785

0 commit comments

Comments
 (0)