diff --git a/lib/webauthn/encoder.rb b/lib/webauthn/encoder.rb index 8d6bf8b5..f1c3adfb 100644 --- a/lib/webauthn/encoder.rb +++ b/lib/webauthn/encoder.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "base64" - module WebAuthn def self.standard_encoder @standard_encoder ||= Encoder.new diff --git a/lib/webauthn/u2f_migrator.rb b/lib/webauthn/u2f_migrator.rb index 9dd9dc33..8de3a3d2 100644 --- a/lib/webauthn/u2f_migrator.rb +++ b/lib/webauthn/u2f_migrator.rb @@ -43,22 +43,36 @@ 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(base64_strict_decode64(@certificate))] end private + def base64_strict_decode64(data) + data.unpack1("m0") + end + + def base64_urlsafe_decode64(data) + if !data.end_with?("=") && data.length % 4 != 0 + data = data.ljust((data.length + 3) & ~3, "=") + data.tr!("-_", "+/") + else + data = data.tr("-_", "+/") + end + data.unpack1("m0") + end + # 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) + base64_urlsafe_decode64(@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 = base64_strict_decode64(@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/spec_helper.rb b/spec/spec_helper.rb index 433ec47c..341e5970 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -186,3 +186,27 @@ def fake_certificate_chain_validation_time(attestation_statement, time) store end end + +def base64_strict_encode64(data) + [data].pack("m0") +end + +def base64_strict_decode64(data) + data.unpack1("m0") +end + +def base64_urlsafe_decode64(data) + if !data.end_with?("=") && data.length % 4 != 0 + data = data.ljust((data.length + 3) & ~3, "=") + data.tr!("-_", "+/") + else + data = data.tr("-_", "+/") + end + data.unpack1("m0") +end + +def base64_urlsafe_encode64(data) + str = base64_strict_encode64(data) + str.tr!("+/", "-_") + str +end diff --git a/spec/webauthn/attestation_statement/android_safetynet_spec.rb b/spec/webauthn/attestation_statement/android_safetynet_spec.rb index 918f856e..564e6b36 100644 --- a/spec/webauthn/attestation_statement/android_safetynet_spec.rb +++ b/spec/webauthn/attestation_statement/android_safetynet_spec.rb @@ -17,7 +17,7 @@ payload, attestation_key, "RS256", - x5c: [Base64.strict_encode64(leaf_certificate.to_der)] + x5c: [base64_strict_encode64(leaf_certificate.to_der)] ) end @@ -26,7 +26,7 @@ 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) { base64_strict_encode64(OpenSSL::Digest::SHA256.digest(authenticator_data_bytes + client_data_hash)) } let(:attestation_key) { create_rsa_key } let(:leaf_certificate) do @@ -63,7 +63,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) { base64_strict_encode64(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 5aac9fd5..8b3d8dbe 100644 --- a/spec/webauthn/authenticator_assertion_response_spec.rb +++ b/spec/webauthn/authenticator_assertion_response_spec.rb @@ -320,12 +320,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: 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]) ) end - let(:original_challenge) { Base64.strict_decode64(assertion_data[:challenge]) } + let(:original_challenge) { base64_strict_decode64(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 3a60b81b..60e342c6 100644 --- a/spec/webauthn/authenticator_attestation_response_spec.rb +++ b/spec/webauthn/authenticator_attestation_response_spec.rb @@ -50,7 +50,7 @@ context "when fido-u2f attestation" do let(:original_challenge) do - Base64.strict_decode64(seeds[:security_key_direct][:credential_creation_options][:challenge]) + base64_strict_decode64(seeds[:security_key_direct][:credential_creation_options][:challenge]) end let(:origin) { "http://localhost:3000" } @@ -59,8 +59,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: base64_strict_decode64(response[:attestation_object]), + client_data_json: base64_strict_decode64(response[:client_data_json]) ) end @@ -98,7 +98,7 @@ let(:origin) { "https://localhost:13010" } let(:original_challenge) do - Base64.strict_decode64( + base64_strict_decode64( seeds[:security_key_packed_self][:credential_creation_options][:challenge] ) end @@ -107,8 +107,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: base64_strict_decode64(response[:attestation_object]), + client_data_json: base64_strict_decode64(response[:client_data_json]) ) end @@ -140,7 +140,7 @@ let(:origin) { "http://localhost:3000" } let(:original_challenge) do - Base64.strict_decode64( + base64_strict_decode64( seeds[:security_key_packed_x5c][:credential_creation_options][:challenge] ) end @@ -149,8 +149,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: base64_strict_decode64(response[:attestation_object]), + client_data_json: base64_strict_decode64(response[:client_data_json]) ) end @@ -185,14 +185,14 @@ context "when TPM attestation" do let(:origin) { seeds[:tpm][:origin] } let(:time) { Time.utc(2019, 8, 13, 22, 6) } - let(:challenge) { Base64.strict_decode64(seeds[:tpm][:credential_creation_options][:challenge]) } + let(:challenge) { base64_strict_decode64(seeds[:tpm][:credential_creation_options][:challenge]) } let(:attestation_response) do response = seeds[:tpm][: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: base64_strict_decode64(response[:attestation_object]), + client_data_json: base64_strict_decode64(response[:client_data_json]) ) end @@ -244,15 +244,15 @@ let(:origin) { "https://7f41ac45.ngrok.io" } let(:original_challenge) do - Base64.strict_decode64(seeds[:android_safetynet_direct][:credential_creation_options][:challenge]) + base64_strict_decode64(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: base64_strict_decode64(response[:attestation_object]), + client_data_json: base64_strict_decode64(response[:client_data_json]) ) end @@ -288,15 +288,15 @@ let(:origin) { seeds[:android_key_direct][:origin] } let(:original_challenge) do - Base64.urlsafe_decode64(seeds[:android_key_direct][:credential_creation_options][:challenge]) + base64_urlsafe_decode64(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: base64_urlsafe_decode64(response[:attestation_object]), + client_data_json: base64_urlsafe_decode64(response[:client_data_json]) ) end @@ -332,15 +332,15 @@ let(:origin) { seeds[:macbook_touch_id][:origin] } let(:original_challenge) do - Base64.urlsafe_decode64(seeds[:macbook_touch_id][:credential_creation_options][:challenge]) + base64_urlsafe_decode64(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: base64_urlsafe_decode64(response[:attestation_object]), + client_data_json: base64_urlsafe_decode64(response[:client_data_json]) ) end @@ -546,7 +546,7 @@ describe "attestation statement verification" do let(:original_challenge) do - Base64.strict_decode64(seeds[:security_key_direct][:credential_creation_options][:challenge]) + base64_strict_decode64(seeds[:security_key_direct][:credential_creation_options][:challenge]) end let(:origin) { "http://localhost:3000" } @@ -555,8 +555,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: base64_strict_decode64(response[:attestation_object]), + client_data_json: base64_strict_decode64(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 574d5b40..5ad2d94d 100644 --- a/spec/webauthn/public_key_credential_with_assertion_spec.rb +++ b/spec/webauthn/public_key_credential_with_assertion_spec.rb @@ -13,16 +13,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) { base64_urlsafe_encode64(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) { base64_urlsafe_encode64(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) { base64_urlsafe_encode64(credential[1]) } let(:credential_sign_count) { credential[2] } let(:assertion_response) do @@ -105,7 +105,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) { base64_urlsafe_encode64(credential_raw_id + "a") } it "fails" do expect do @@ -132,7 +132,7 @@ end context "when challenge is invalid" do - let(:challenge) { Base64.urlsafe_encode64("another challenge") } + let(:challenge) { base64_urlsafe_encode64("another challenge") } it "fails" do expect do @@ -273,7 +273,7 @@ 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 { |v| base64_strict_decode64(v) } ) end diff --git a/spec/webauthn/public_key_credential_with_attestation_spec.rb b/spec/webauthn/public_key_credential_with_attestation_spec.rb index 9f03131c..20a7c39f 100644 --- a/spec/webauthn/public_key_credential_with_attestation_spec.rb +++ b/spec/webauthn/public_key_credential_with_attestation_spec.rb @@ -21,7 +21,7 @@ end let(:type) { "public-key" } - let(:id) { Base64.urlsafe_encode64(raw_id) } + let(:id) { base64_urlsafe_encode64(raw_id) } let(:raw_id) { SecureRandom.random_bytes(16) } let(:authenticator_attachment) { 'platform' } @@ -35,7 +35,7 @@ end let(:client) { WebAuthn::FakeClient.new(origin, encoding: false) } - let(:challenge) { Base64.urlsafe_encode64(raw_challenge) } + let(:challenge) { base64_urlsafe_encode64(raw_challenge) } let(:raw_challenge) { fake_challenge } let(:origin) { fake_origin } @@ -79,7 +79,7 @@ end context "because it is not the base64url of raw id" do - let(:id) { Base64.urlsafe_encode64(raw_id + "a") } + let(:id) { base64_urlsafe_encode64(raw_id + "a") } it "fails" do expect { public_key_credential.verify(challenge) }.to raise_error(RuntimeError) @@ -98,7 +98,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(base64_urlsafe_encode64("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..77d365f1 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]) + base64_strict_decode64(seeds[:u2f_migration][:stored_credential][:public_key]) end let(:cose_public_key) do - Base64.urlsafe_decode64( + base64_urlsafe_decode64( "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]) + base64_strict_decode64(seeds[:u2f_migration][:assertion][:response][:signature]) end let(:authenticator_data) do - Base64.strict_decode64(seeds[:u2f_migration][:assertion][:response][:authenticator_data]) + base64_strict_decode64(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]) + base64_strict_decode64(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..28eb2c97 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(base64_strict_encode64(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(base64_strict_decode64("FtOd9t3mxj6sLFkNCLzv5qS9l52MipHznrsZ+uwtHQY=")) + expect(public_key.y).to eq(base64_strict_decode64("np4zBpD5zhdSq1wKPvhzEoKJvFuYel1cpdTCzpahrBA=")) end it "returns the signature counter" do diff --git a/webauthn.gemspec b/webauthn.gemspec index 219a532e..5ef2d721 100644 --- a/webauthn.gemspec +++ b/webauthn.gemspec @@ -42,7 +42,6 @@ Gem::Specification.new do |spec| spec.add_dependency "safety_net_attestation", "~> 0.4.0" spec.add_dependency "tpm-key_attestation", "~> 0.12.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"