diff --git a/.gitignore b/.gitignore index 7045aa8f..839207f3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ /gemfiles/*.gemfile.lock .byebug_history /spec/conformance/metadata.zip +.idea/ +.ruby-version diff --git a/Gemfile b/Gemfile index 8eea8c7f..a756b550 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in webauthn.gemspec gemspec + +group :development, :test do + gem 'rspec' +end diff --git a/lib/webauthn/encoder.rb b/lib/webauthn/encoder.rb index 8d6bf8b5..e89e51a2 100644 --- a/lib/webauthn/encoder.rb +++ b/lib/webauthn/encoder.rb @@ -1,55 +1,112 @@ -# frozen_string_literal: true - require "base64" module WebAuthn - def self.standard_encoder - @standard_encoder ||= Encoder.new + module Config + module Encoder + # Default encoding type used for WebAuthn operations + DEFAULT_ENCODING = :base64url + + # Supported encoding types and their corresponding Base64 methods + ENCODINGS = { + base64: :strict, + base64url: :urlsafe + }.freeze + + # Error message template for unsupported encoding types + INVALID_ENCODING_ERROR = "Unsupported or unknown encoding: %s".freeze + end end - class Encoder - # https://www.w3.org/TR/webauthn-2/#base64url-encoding - STANDARD_ENCODING = :base64url + # Custom error class for encoding-related errors + class EncodingError < StandardError; end + # Handles encoding and decoding of WebAuthn data + class Encoder attr_reader :encoding - def initialize(encoding = STANDARD_ENCODING) + # Initialize encoder with specified encoding type + # @param encoding [Symbol] the encoding type to use (:base64url by default) + def initialize(encoding = Config::Encoder::DEFAULT_ENCODING) @encoding = encoding end + # Encode data using the specified encoding type + # @param data [String] the data to encode + # @return [String] encoded data 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 + return data if skip_encoding? + + validate_encoding! + send("encode_#{encoding}", data) end + # Decode data using the specified encoding type + # @param data [String] the data to decode + # @return [String] decoded data 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 + return data if skip_encoding? + + validate_encoding! + send("decode_#{encoding}", data) + end + + private + + # Check if encoding should be skipped + # @return [Boolean] true if encoding is nil or false + def skip_encoding? + encoding.nil? || encoding == false + end + + # Validate that the current encoding type is supported + # @raise [EncodingError] if encoding type is not supported + def validate_encoding! + return if Config::Encoder::ENCODINGS.key?(encoding) + + raise EncodingError, Config::Encoder::INVALID_ENCODING_ERROR % encoding end + + # Encode data using standard Base64 encoding + # @param data [String] the data to encode + # @return [String] Base64 encoded data + def encode_base64(data) + Base64.strict_encode64(data) + end + + # Encode data using URL-safe Base64 encoding without padding + # @param data [String] the data to encode + # @return [String] URL-safe Base64 encoded data + def encode_base64url(data) + Base64.urlsafe_encode64(data, padding: false) + end + + # Decode standard Base64 encoded data + # @param data [String] the Base64 encoded data + # @return [String] decoded data + def decode_base64(data) + Base64.strict_decode64(data) + end + + # Decode URL-safe Base64 encoded data + # @param data [String] the URL-safe Base64 encoded data + # @return [String] decoded data + def decode_base64url(data) + Base64.urlsafe_decode64(ensure_padding(data)) + end + + # Ensure proper Base64 padding + # @param data [String] the data to pad + # @return [String] properly padded data + def ensure_padding(data) + return data if data.end_with?("=") + + data.ljust((data.length + 3) & ~3, "=") + end + end + + # Factory method to create a standard encoder instance + # @return [Encoder] a new encoder instance with default settings + def self.standard_encoder + @standard_encoder ||= Encoder.new end end diff --git a/lib/webauthn/relying_party.rb b/lib/webauthn/relying_party.rb index 6a667c48..2c0cfbd9 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: Config::Encoder::DEFAULT_ENCODING, origin: nil, id: nil, name: nil,