Skip to content

Remove base64 dependency #459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 5 additions & 45 deletions lib/webauthn/encoder.rb
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions lib/webauthn/encoders.rb
Original file line number Diff line number Diff line change
@@ -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("-_", "+/")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that in the base64 gem, the method tr has a bang in the if branch, but not in the else branch 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! I'll open a separate PR to fix that 🙂

data.unpack1("m0")
end
end

class NullEncoder
def self.encode(data)
data
end

def self.decode(data)
data
end
end
end
end
2 changes: 1 addition & 1 deletion lib/webauthn/relying_party.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions lib/webauthn/u2f_migrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,24 @@ 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

# 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,
Expand Down
11 changes: 7 additions & 4 deletions spec/webauthn/attestation_statement/android_safetynet_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

require "spec_helper"

require "base64"
require "jwt"
require "openssl"
require "webauthn/attestation_statement/android_safetynet"
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions spec/webauthn/authenticator_assertion_response_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 26 additions & 25 deletions spec/webauthn/authenticator_attestation_response_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
require "spec_helper"
require "support/seeds"

require "base64"
require "webauthn/authenticator_attestation_response"
require "openssl"

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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" }
Expand All @@ -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

Expand Down
Loading