diff --git a/lib/webauthn/encoder.rb b/lib/webauthn/encoder.rb index 8d6bf8b5..149201fb 100644 --- a/lib/webauthn/encoder.rb +++ b/lib/webauthn/encoder.rb @@ -1,55 +1,15 @@ # frozen_string_literal: true -require "base64" +require "webauthn/encoders" module WebAuthn - def self.standard_encoder - @standard_encoder ||= Encoder.new - end - class Encoder - # https://www.w3.org/TR/webauthn-2/#base64url-encoding - STANDARD_ENCODING = :base64url - - attr_reader :encoding + extend Forwardable - def initialize(encoding = STANDARD_ENCODING) - @encoding = encoding - end - - def encode(data) - case encoding - when :base64 - [data].pack("m0") # Base64.strict_encode64(data) - when :base64url - data = [data].pack("m0") # Base64.urlsafe_encode64(data, padding: false) - data.chomp!("==") or data.chomp!("=") - data.tr!("+/", "-_") - data - when nil, false - data - else - raise "Unsupported or unknown encoding: #{encoding}" - end - end + def_delegators :@encoder_klass, :encode, :decode - def decode(data) - case encoding - when :base64 - data.unpack1("m0") # Base64.strict_decode64(data) - when :base64url - if !data.end_with?("=") && data.length % 4 != 0 # Base64.urlsafe_decode64(data) - data = data.ljust((data.length + 3) & ~3, "=") - data.tr!("-_", "+/") - else - data = data.tr("-_", "+/") - end - data.unpack1("m0") - when nil, false - data - else - raise "Unsupported or unknown encoding: #{encoding}" - end + def initialize(encoding = Encoders::STANDARD_ENCODING) + @encoder_klass = Encoders.lookup(encoding) end end end diff --git a/lib/webauthn/encoders.rb b/lib/webauthn/encoders.rb new file mode 100644 index 00000000..7ea0928d --- /dev/null +++ b/lib/webauthn/encoders.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module WebAuthn + def self.standard_encoder + @standard_encoder ||= Encoders.lookup(Encoders::STANDARD_ENCODING) + end + + module Encoders + # https://www.w3.org/TR/webauthn-2/#base64url-encoding + STANDARD_ENCODING = :base64url + + class << self + def lookup(encoding) + case encoding + when :base64 + Base64Encoder + when :base64url + Base64UrlEncoder + when nil, false + NullEncoder + else + raise "Unsupported or unknown encoding: #{encoding}" + end + end + end + + class Base64Encoder + def self.encode(data) + [data].pack("m0") # Base64.strict_encode64(data) + end + + def self.decode(data) + data.unpack1("m0") # Base64.strict_decode64(data) + end + end + + class Base64UrlEncoder + def self.encode(data) + data = [data].pack("m0") # Base64.urlsafe_encode64(data, padding: false) + data.chomp!("==") or data.chomp!("=") + data.tr!("+/", "-_") + data + end + + def self.decode(data) + if !data.end_with?("=") && data.length % 4 != 0 # Base64.urlsafe_decode64(data) + data = data.ljust((data.length + 3) & ~3, "=") + end + + data = data.tr("-_", "+/") + data.unpack1("m0") + end + end + + class NullEncoder + def self.encode(data) + data + end + + def self.decode(data) + data + end + end + end +end diff --git a/lib/webauthn/relying_party.rb b/lib/webauthn/relying_party.rb index 06589d9f..e4294a0c 100644 --- a/lib/webauthn/relying_party.rb +++ b/lib/webauthn/relying_party.rb @@ -17,7 +17,7 @@ def self.if_pss_supported(algorithm) def initialize( algorithms: DEFAULT_ALGORITHMS.dup, - encoding: WebAuthn::Encoder::STANDARD_ENCODING, + encoding: WebAuthn::Encoders::STANDARD_ENCODING, allowed_origins: nil, origin: nil, id: nil, diff --git a/lib/webauthn/u2f_migrator.rb b/lib/webauthn/u2f_migrator.rb index 9dd9dc33..d33ddc7f 100644 --- a/lib/webauthn/u2f_migrator.rb +++ b/lib/webauthn/u2f_migrator.rb @@ -43,7 +43,9 @@ def attestation_type end def attestation_trust_path - @attestation_trust_path ||= [OpenSSL::X509::Certificate.new(Base64.strict_decode64(@certificate))] + @attestation_trust_path ||= [ + OpenSSL::X509::Certificate.new(WebAuthn::Encoders::Base64Encoder.decode(@certificate)) + ] end private @@ -51,14 +53,14 @@ def attestation_trust_path # https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#u2f-authenticatorMakeCredential-interoperability # Let credentialId be a credentialIdLength byte array initialized with CTAP1/U2F response key handle bytes. def credential_id - Base64.urlsafe_decode64(@key_handle) + WebAuthn::Encoders::Base64UrlEncoder.decode(@key_handle) end # Let x9encodedUserPublicKey be the user public key returned in the U2F registration response message [U2FRawMsgs]. # Let coseEncodedCredentialPublicKey be the result of converting x9encodedUserPublicKey’s value from ANS X9.62 / # Sec-1 v2 uncompressed curve point representation [SEC1V2] to COSE_Key representation ([RFC8152] Section 7). def credential_cose_key - decoded_public_key = Base64.strict_decode64(@public_key) + decoded_public_key = WebAuthn::Encoders::Base64Encoder.decode(@public_key) if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(decoded_public_key) COSE::Key::EC2.new( alg: COSE::Algorithm.by_name("ES256").id, diff --git a/spec/webauthn/attestation_statement/android_safetynet_spec.rb b/spec/webauthn/attestation_statement/android_safetynet_spec.rb index 918f856e..efe6e4c2 100644 --- a/spec/webauthn/attestation_statement/android_safetynet_spec.rb +++ b/spec/webauthn/attestation_statement/android_safetynet_spec.rb @@ -2,7 +2,6 @@ require "spec_helper" -require "base64" require "jwt" require "openssl" require "webauthn/attestation_statement/android_safetynet" @@ -17,7 +16,7 @@ payload, attestation_key, "RS256", - x5c: [Base64.strict_encode64(leaf_certificate.to_der)] + x5c: [WebAuthn::Encoders::Base64Encoder.encode(leaf_certificate.to_der)] ) end @@ -26,7 +25,11 @@ end let(:timestamp) { Time.now } let(:cts_profile_match) { true } - let(:nonce) { Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(authenticator_data_bytes + client_data_hash)) } + let(:nonce) do + WebAuthn::Encoders::Base64Encoder.encode( + OpenSSL::Digest::SHA256.digest(authenticator_data_bytes + client_data_hash) + ) + end let(:attestation_key) { create_rsa_key } let(:leaf_certificate) do @@ -63,7 +66,7 @@ end context "when nonce is not set to the base64 of the SHA256 of authData + clientDataHash" do - let(:nonce) { Base64.strict_encode64(OpenSSL::Digest.digest("SHA256", "something else")) } + let(:nonce) { WebAuthn::Encoders::Base64Encoder.encode(OpenSSL::Digest.digest("SHA256", "something else")) } it "returns false" do expect(statement.valid?(authenticator_data, client_data_hash)).to be_falsy diff --git a/spec/webauthn/authenticator_assertion_response_spec.rb b/spec/webauthn/authenticator_assertion_response_spec.rb index b0d3f95d..dab09d53 100644 --- a/spec/webauthn/authenticator_assertion_response_spec.rb +++ b/spec/webauthn/authenticator_assertion_response_spec.rb @@ -518,12 +518,12 @@ let(:assertion_data) { seeds[:u2f_migration][:assertion] } let(:assertion_response) do WebAuthn::AuthenticatorAssertionResponse.new( - client_data_json: Base64.strict_decode64(assertion_data[:response][:client_data_json]), - authenticator_data: Base64.strict_decode64(assertion_data[:response][:authenticator_data]), - signature: Base64.strict_decode64(assertion_data[:response][:signature]) + client_data_json: WebAuthn::Encoders::Base64Encoder.decode(assertion_data[:response][:client_data_json]), + authenticator_data: WebAuthn::Encoders::Base64Encoder.decode(assertion_data[:response][:authenticator_data]), + signature: WebAuthn::Encoders::Base64Encoder.decode(assertion_data[:response][:signature]) ) end - let(:original_challenge) { Base64.strict_decode64(assertion_data[:challenge]) } + let(:original_challenge) { WebAuthn::Encoders::Base64Encoder.decode(assertion_data[:challenge]) } context "when correct FIDO AppID is given as rp_id" do it "verifies" do diff --git a/spec/webauthn/authenticator_attestation_response_spec.rb b/spec/webauthn/authenticator_attestation_response_spec.rb index 493c008f..90af0664 100644 --- a/spec/webauthn/authenticator_attestation_response_spec.rb +++ b/spec/webauthn/authenticator_attestation_response_spec.rb @@ -3,7 +3,6 @@ require "spec_helper" require "support/seeds" -require "base64" require "webauthn/authenticator_attestation_response" require "openssl" @@ -114,7 +113,7 @@ context "when fido-u2f attestation" do let(:original_challenge) do - Base64.strict_decode64(seeds[:security_key_direct][:credential_creation_options][:challenge]) + WebAuthn::Encoders::Base64Encoder.decode(seeds[:security_key_direct][:credential_creation_options][:challenge]) end context "when there is a single origin" do @@ -124,8 +123,8 @@ response = seeds[:security_key_direct][:authenticator_attestation_response] WebAuthn::AuthenticatorAttestationResponse.new( - attestation_object: Base64.strict_decode64(response[:attestation_object]), - client_data_json: Base64.strict_decode64(response[:client_data_json]) + attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]), + client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json]) ) end @@ -194,7 +193,7 @@ let(:origin) { "https://localhost:13010" } let(:original_challenge) do - Base64.strict_decode64( + WebAuthn::Encoders::Base64Encoder.decode( seeds[:security_key_packed_self][:credential_creation_options][:challenge] ) end @@ -203,8 +202,8 @@ response = seeds[:security_key_packed_self][:authenticator_attestation_response] WebAuthn::AuthenticatorAttestationResponse.new( - attestation_object: Base64.strict_decode64(response[:attestation_object]), - client_data_json: Base64.strict_decode64(response[:client_data_json]) + attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]), + client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json]) ) end @@ -234,7 +233,7 @@ let(:origin) { "http://localhost:3000" } let(:original_challenge) do - Base64.strict_decode64( + WebAuthn::Encoders::Base64Encoder.decode( seeds[:security_key_packed_x5c][:credential_creation_options][:challenge] ) end @@ -243,8 +242,8 @@ response = seeds[:security_key_packed_x5c][:authenticator_attestation_response] WebAuthn::AuthenticatorAttestationResponse.new( - attestation_object: Base64.strict_decode64(response[:attestation_object]), - client_data_json: Base64.strict_decode64(response[:client_data_json]) + attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]), + client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json]) ) end @@ -274,14 +273,14 @@ context "when TPM attestation" do let(:origin) { seeds[:tpm][:origin] } let(:time) { Time.utc(2019, 8, 13, 22, 6) } - let(:challenge) { Base64.urlsafe_decode64(seeds[:tpm][:credential_creation_options][:challenge]) } + let(:challenge) { WebAuthn::Encoders::Base64UrlEncoder.decode(seeds[:tpm][:credential_creation_options][:challenge]) } let(:attestation_response) do response = seeds[:tpm][:authenticator_attestation_response] WebAuthn::AuthenticatorAttestationResponse.new( - attestation_object: Base64.urlsafe_decode64(response[:attestation_object]), - client_data_json: Base64.urlsafe_decode64(response[:client_data_json]) + attestation_object: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:attestation_object]), + client_data_json: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:client_data_json]) ) end @@ -334,15 +333,17 @@ let(:origin) { "https://7f41ac45.ngrok.io" } let(:original_challenge) do - Base64.strict_decode64(seeds[:android_safetynet_direct][:credential_creation_options][:challenge]) + WebAuthn::Encoders::Base64Encoder.decode( + seeds[:android_safetynet_direct][:credential_creation_options][:challenge] + ) end let(:attestation_response) do response = seeds[:android_safetynet_direct][:authenticator_attestation_response] WebAuthn::AuthenticatorAttestationResponse.new( - attestation_object: Base64.strict_decode64(response[:attestation_object]), - client_data_json: Base64.strict_decode64(response[:client_data_json]) + attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]), + client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json]) ) end @@ -371,15 +372,15 @@ context "when android-key attestation" do let(:original_challenge) do - Base64.urlsafe_decode64(seeds[:android_key_direct][:credential_creation_options][:challenge]) + WebAuthn::Encoders::Base64UrlEncoder.decode(seeds[:android_key_direct][:credential_creation_options][:challenge]) end let(:attestation_response) do response = seeds[:android_key_direct][:authenticator_attestation_response] WebAuthn::AuthenticatorAttestationResponse.new( - attestation_object: Base64.urlsafe_decode64(response[:attestation_object]), - client_data_json: Base64.urlsafe_decode64(response[:client_data_json]) + attestation_object: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:attestation_object]), + client_data_json: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:client_data_json]) ) end @@ -468,15 +469,15 @@ let(:origin) { seeds[:macbook_touch_id][:origin] } let(:original_challenge) do - Base64.urlsafe_decode64(seeds[:macbook_touch_id][:credential_creation_options][:challenge]) + WebAuthn::Encoders::Base64UrlEncoder.decode(seeds[:macbook_touch_id][:credential_creation_options][:challenge]) end let(:attestation_response) do response = seeds[:macbook_touch_id][:authenticator_attestation_response] WebAuthn::AuthenticatorAttestationResponse.new( - attestation_object: Base64.urlsafe_decode64(response[:attestation_object]), - client_data_json: Base64.urlsafe_decode64(response[:client_data_json]) + attestation_object: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:attestation_object]), + client_data_json: WebAuthn::Encoders::Base64UrlEncoder.decode(response[:client_data_json]) ) end @@ -766,7 +767,7 @@ describe "attestation statement verification" do let(:original_challenge) do - Base64.strict_decode64(seeds[:security_key_direct][:credential_creation_options][:challenge]) + WebAuthn::Encoders::Base64Encoder.decode(seeds[:security_key_direct][:credential_creation_options][:challenge]) end let(:origin) { "http://localhost:3000" } @@ -775,8 +776,8 @@ response = seeds[:security_key_direct][:authenticator_attestation_response] WebAuthn::AuthenticatorAttestationResponse.new( - attestation_object: Base64.strict_decode64(response[:attestation_object]), - client_data_json: Base64.strict_decode64(response[:client_data_json]) + attestation_object: WebAuthn::Encoders::Base64Encoder.decode(response[:attestation_object]), + client_data_json: WebAuthn::Encoders::Base64Encoder.decode(response[:client_data_json]) ) end diff --git a/spec/webauthn/public_key_credential_with_assertion_spec.rb b/spec/webauthn/public_key_credential_with_assertion_spec.rb index 34b147c9..212c7e92 100644 --- a/spec/webauthn/public_key_credential_with_assertion_spec.rb +++ b/spec/webauthn/public_key_credential_with_assertion_spec.rb @@ -2,7 +2,6 @@ require "spec_helper" -require "base64" require "securerandom" require "webauthn/authenticator_assertion_response" require "webauthn/configuration" @@ -13,16 +12,16 @@ RSpec.describe "PublicKeyCredentialWithAssertion" do describe "#verify" do let(:client) { WebAuthn::FakeClient.new(origin, encoding: false) } - let(:challenge) { Base64.urlsafe_encode64(raw_challenge) } + let(:challenge) { WebAuthn::Encoders::Base64UrlEncoder.encode(raw_challenge) } let(:raw_challenge) { fake_challenge } let(:origin) { fake_origin } let!(:credential) { create_credential(client: client) } let(:credential_raw_id) { credential[0] } - let(:credential_id) { Base64.urlsafe_encode64(credential_raw_id) } + let(:credential_id) { WebAuthn::Encoders::Base64UrlEncoder.encode(credential_raw_id) } let(:credential_type) { "public-key" } let(:credential_authenticator_attachment) { 'platform' } - let(:credential_public_key) { Base64.urlsafe_encode64(credential[1]) } + let(:credential_public_key) { WebAuthn::Encoders::Base64UrlEncoder.encode(credential[1]) } let(:credential_sign_count) { credential[2] } let(:assertion_response) do @@ -105,7 +104,7 @@ end context "because it is not the base64url of raw id" do - let(:credential_id) { Base64.urlsafe_encode64(credential_raw_id + "a") } + let(:credential_id) { WebAuthn::Encoders::Base64UrlEncoder.encode(credential_raw_id + "a") } it "fails" do expect do @@ -132,7 +131,7 @@ end context "when challenge is invalid" do - let(:challenge) { Base64.urlsafe_encode64("another challenge") } + let(:challenge) { WebAuthn::Encoders::Base64UrlEncoder.encode("another challenge") } it "fails" do expect do @@ -273,7 +272,9 @@ let(:assertion_response) do WebAuthn::AuthenticatorAssertionResponse.new( - **seeds[:u2f_migration][:assertion][:response].transform_values { |v| Base64.strict_decode64(v) } + **seeds[:u2f_migration][:assertion][:response].transform_values do |v| + WebAuthn::Encoders::Base64Encoder.decode(v) + end ) end diff --git a/spec/webauthn/public_key_credential_with_attestation_spec.rb b/spec/webauthn/public_key_credential_with_attestation_spec.rb index 60f195ac..d3024168 100644 --- a/spec/webauthn/public_key_credential_with_attestation_spec.rb +++ b/spec/webauthn/public_key_credential_with_attestation_spec.rb @@ -2,7 +2,6 @@ require "spec_helper" -require "base64" require "securerandom" require "webauthn/authenticator_attestation_response" require "webauthn/configuration" @@ -21,7 +20,7 @@ end let(:type) { "public-key" } - let(:id) { Base64.urlsafe_encode64(raw_id) } + let(:id) { WebAuthn::Encoders::Base64UrlEncoder.encode(raw_id) } let(:raw_id) { SecureRandom.random_bytes(16) } let(:authenticator_attachment) { 'platform' } @@ -35,7 +34,7 @@ end let(:client) { WebAuthn::FakeClient.new(origin, encoding: false) } - let(:challenge) { Base64.urlsafe_encode64(raw_challenge) } + let(:challenge) { WebAuthn::Encoders::Base64UrlEncoder.encode(raw_challenge) } let(:raw_challenge) { fake_challenge } let(:origin) { fake_origin } @@ -79,7 +78,7 @@ end context "because it is not the base64url of raw id" do - let(:id) { Base64.urlsafe_encode64(raw_id + "a") } + let(:id) { WebAuthn::Encoders::Base64UrlEncoder.encode(raw_id + "a") } it "fails" do expect { public_key_credential.verify(challenge) }.to raise_error(RuntimeError) @@ -98,7 +97,7 @@ context "when challenge value is invalid" do it "fails" do expect { - public_key_credential.verify(Base64.urlsafe_encode64("another challenge")) + public_key_credential.verify(WebAuthn::Encoders::Base64UrlEncoder.encode("another challenge")) }.to raise_error(WebAuthn::ChallengeVerificationError) end end diff --git a/spec/webauthn/public_key_spec.rb b/spec/webauthn/public_key_spec.rb index 4d55a63b..0814d41c 100644 --- a/spec/webauthn/public_key_spec.rb +++ b/spec/webauthn/public_key_spec.rb @@ -9,10 +9,10 @@ RSpec.describe "PublicKey" do let(:uncompressed_point_public_key) do - Base64.strict_decode64(seeds[:u2f_migration][:stored_credential][:public_key]) + WebAuthn::Encoders::Base64Encoder.decode(seeds[:u2f_migration][:stored_credential][:public_key]) end let(:cose_public_key) do - Base64.urlsafe_decode64( + WebAuthn::Encoders::Base64UrlEncoder.decode( "pQECAyYgASFYIPJKd_-Rl0QtQwbLggjGC_EbUFIMriCkdc2yuaukkBuNIlggaBsBjCwnMzFL7OUGJNm4b-HVpFNUa_NbsHGARuYKHfU" ) end @@ -94,14 +94,14 @@ context "when signature was signed with public key" do let(:signature) do - Base64.strict_decode64(seeds[:u2f_migration][:assertion][:response][:signature]) + WebAuthn::Encoders::Base64Encoder.decode(seeds[:u2f_migration][:assertion][:response][:signature]) end let(:authenticator_data) do - Base64.strict_decode64(seeds[:u2f_migration][:assertion][:response][:authenticator_data]) + WebAuthn::Encoders::Base64Encoder.decode(seeds[:u2f_migration][:assertion][:response][:authenticator_data]) end let(:client_data_hash) do WebAuthn::ClientData.new( - Base64.strict_decode64(seeds[:u2f_migration][:assertion][:response][:client_data_json]) + WebAuthn::Encoders::Base64Encoder.decode(seeds[:u2f_migration][:assertion][:response][:client_data_json]) ).hash end let(:verification_data) { authenticator_data + client_data_hash } diff --git a/spec/webauthn/u2f_migrator_spec.rb b/spec/webauthn/u2f_migrator_spec.rb index d9dfa962..dbae3b1a 100644 --- a/spec/webauthn/u2f_migrator_spec.rb +++ b/spec/webauthn/u2f_migrator_spec.rb @@ -21,7 +21,7 @@ let(:app_id) { URI("https://f69df4d9.ngrok.io") } it "returns the credential ID" do - expect(Base64.strict_encode64(u2f_migrator.credential.id)) + expect(WebAuthn::Encoders::Base64Encoder.encode(u2f_migrator.credential.id)) .to eq("1a9tIwwYiYNdmfmxVaksOkxKapK2HtDNSsL4MssbCHILhkMzA0xZYk5IHmBljyblTQ/SnsQea+QEMzgTN2L1Mw==") end @@ -30,8 +30,8 @@ expect(public_key.alg).to eq(-7) expect(public_key.crv).to eq(1) - expect(public_key.x).to eq(Base64.strict_decode64("FtOd9t3mxj6sLFkNCLzv5qS9l52MipHznrsZ+uwtHQY=")) - expect(public_key.y).to eq(Base64.strict_decode64("np4zBpD5zhdSq1wKPvhzEoKJvFuYel1cpdTCzpahrBA=")) + expect(public_key.x).to eq(WebAuthn::Encoders::Base64Encoder.decode("FtOd9t3mxj6sLFkNCLzv5qS9l52MipHznrsZ+uwtHQY=")) + expect(public_key.y).to eq(WebAuthn::Encoders::Base64Encoder.decode("np4zBpD5zhdSq1wKPvhzEoKJvFuYel1cpdTCzpahrBA=")) end it "returns the signature counter" do diff --git a/webauthn.gemspec b/webauthn.gemspec index dac6b913..fc299f27 100644 --- a/webauthn.gemspec +++ b/webauthn.gemspec @@ -41,7 +41,6 @@ Gem::Specification.new do |spec| spec.add_dependency "safety_net_attestation", "~> 0.4.0" spec.add_dependency "tpm-key_attestation", "~> 0.14.0" - spec.add_development_dependency "base64", ">= 0.1.0" spec.add_development_dependency "bundler", ">= 1.17", "< 3.0" spec.add_development_dependency "byebug", "~> 11.0" spec.add_development_dependency "rake", "~> 13.0"