Skip to content

Commit

Permalink
Merge pull request #18 from copiousfreetime/feature/support-argon2
Browse files Browse the repository at this point in the history
Feature/support argon2
  • Loading branch information
copiousfreetime authored Feb 4, 2024
2 parents f6635f6 + fb6897e commit e4296e9
Show file tree
Hide file tree
Showing 32 changed files with 276 additions and 106 deletions.
1 change: 0 additions & 1 deletion .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ blocks:
- 3.1.4
- 3.2.2
- 3.3.0
- jruby-9.3.13.0
- jruby-9.4.5.0
- truffleruby-23.1.2
commands:
Expand Down
15 changes: 13 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
PATH
remote: .
specs:
htauth (2.2.0)
htauth (2.3.0)
base64 (~> 0.2)
bcrypt (~> 3.1)

GEM
remote: https://rubygems.org/
specs:
argon2 (2.3.0)
ffi (~> 1.15)
ffi-compiler (~> 1.0)
base64 (0.2.0)
bcrypt (3.1.20)
bcrypt (3.1.20-java)
builder (3.2.4)
docile (1.4.0)
ffi (1.16.3)
ffi (1.16.3-java)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
jar-dependencies (0.4.1)
minitest (5.21.1)
minitest (5.21.2)
minitest-junit (1.1.0)
builder (~> 3.2)
minitest (~> 5.11)
Expand All @@ -38,6 +48,7 @@ PLATFORMS
ruby

DEPENDENCIES
argon2 (~> 2.3)
htauth!
minitest (~> 5.21)
minitest-junit (~> 1.1)
Expand Down
5 changes: 3 additions & 2 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Changelog
## Version 2.3.0 - 2024-01-XX
## Version 2.3.0 - 2024-02-03

* Update ruby version - dropping official support for 2.6 and 2.7
* Add support for argon2 encryption [#18](https://github.com/copiousfreetime/htauth/pull/18)
* Update supported ruby version, now supporting ruby 3.x only
* Semaphore updates

## Version 2.2.0 - 2023-02-06
Expand Down
82 changes: 51 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,45 @@

## DESCRIPTION

HTAuth is a pure ruby replacement for the Apache support programs htdigest and
htpasswd. Command line and API access are provided for access to htdigest and
htpasswd files.
HTAuth provides an API and commandline tools for managing Apache/httpd style
htpasswd and htdigest files.

## FEATURES

HTAuth provides to drop in commands *htdigest-ruby* and *htpasswd-ruby* that
can manipulate the digest and passwd files in the same manner as Apache's
original commands.
HTAuth provides an API allowing direct manipulation of Apache/httpd style
`htdigest` and `htpasswd` files. Supporting full programmatic manipulation and
authentication of user credentials.

*htdigest-ruby* and *htpasswd-ruby* are command line compatible with *htdigest*
and *htpasswd*. They support the same exact same command line options as the
originals, and have some extras.
HTAuth also includes drop-in, commandline compatible replacements for the Apache
utilities `htpasswd` and `htdigest` with the respective `htpasswd-ruby` and
`htdigest-ruby` commands.

Additionally, you can access all the functionality of *htdigest-ruby* and
*htpasswd-ruby* through an API.
Additionally, support for the [argon2](https://github.com/technion/ruby-argon2)
password hashing algorithm is provided for most platforms.

## SYNOPSIS

### API Usage

HTAuth::DigestFile.open("some.htdigest") do |df|
df.add_or_update('someuser', 'myrealm', 'a password')
df.delete('someolduser', 'myotherrealm')
end

HTAuth::PasswdFile.open("some.htpasswd", HTAuth::File::CREATE) do |pf|
pf.add('someuser', 'a password', 'md5')
pf.add('someotheruser', 'a different password', 'sha1')
end

HTAuth::PasswdFile.open("some.htpasswd", HTAuth::File::ALTER) do |pf|
pf.update('someuser', 'a password', 'bcrypt')
end

HTAuth::PasswdFile.open("some.htpasswd") do |pf|
pf.authenticated?('someuser', 'a password')
end


### htpasswd-ruby command line application

Usage:
Expand All @@ -35,7 +55,8 @@ Additionally, you can access all the functionality of *htdigest-ruby* and
htpasswd-ruby -n[imBdps] [-C cost] username
htpasswd-ruby -nb[mBdps] [-C cost] username password

-b, --batch Batch mode, get the password from the command line, rather than prompt
--argon2 Force argon2 encryption of the password.
-b, --batch Batch mode, get the password from the command line, rather than prompt.
-B, --bcrypt Force bcrypt encryption of the password.
-C, --cost COST Set the computing time used for the bcrypt algorithm
(higher is more secure but slower, default: 5, valid: 4 to 31).
Expand All @@ -49,9 +70,7 @@ Additionally, you can access all the functionality of *htdigest-ruby* and
-p, --plaintext Do not encrypt the password (plaintext).
-s, --sha1 Force SHA encryption of the password.
-v, --version Show version info.
--verify Verify password for the specified user

The SHA algorithm does not use a salt and is less secure than the MD5 algorithm.
--verify Verify password for the specified user.

### htdigest-ruby command line application

Expand All @@ -61,30 +80,31 @@ Additionally, you can access all the functionality of *htdigest-ruby* and
-h, --help Display this help.
-v, --version Show version info.

### API Usage
## Supported Hash Algorithms

HTAuth::DigestFile.open("some.htdigest") do |df|
df.add_or_update('someuser', 'myrealm', 'a password')
df.delete('someolduser', 'myotherrealm')
end
Out of the box, `htauth` supports the classic algorithms that ship with Apache
`htpasswd`.

HTAuth::PasswdFile.open("some.htpasswd", HTAuth::File::CREATE) do |pf|
pf.add('someuser', 'a password', 'md5')
pf.add('someotheruser', 'a different password', 'sha1')
end
- Built in
- Generally accepted
- MD5 (default for compatibility reasons)
- bcrypt (probably the better choice)

HTAuth::PasswdFile.open("some.htpasswd", HTAuth::File::ALTER) do |pf|
pf.update('someuser', 'a password', 'bcrypt')
end
- **Not Recommended** - available only for backwards compatibility with `htpasswd`
- SHA1
- crypt
- plaintext

HTAuth::PasswdFile.open("some.htpasswd") do |pf|
pf.authenticated?('someuser', 'a password')
end
- Available with the installation of additional libraries:
- argon2 - to use, add `gem 'argon2'` to your `Gemfile`. `argon2` will
now be a valid algorithm to use in `HTAuth::PasswdFile` API. Currently
argon2 is not supported on windows as the upstream `argon2` gem does not
support windows.

## CREDITS

* [The Apache Software Foundation](http://www.apache.org/)
* all the folks who contributed to htdigest and htpassword
* all the folks who contributed to htdigest and htpasswd

## MIT LICENSE

Expand Down
2 changes: 2 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ This.homepage = "http://github.com/copiousfreetime/#{ This.name }"

This.ruby_gemspec do |spec|
spec.add_dependency( 'bcrypt', '~> 3.1' )
spec.add_dependency( 'base64', '~> 0.2' )

spec.add_development_dependency( 'argon2' , '~> 2.3')
spec.add_development_dependency( 'rake' , '~> 13.1')
spec.add_development_dependency( 'minitest' , '~> 5.21' )
spec.add_development_dependency( 'minitest-junit' , '~> 1.1' )
Expand Down
2 changes: 1 addition & 1 deletion bin/htdigest-ruby
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby

begin
require 'htauth/cli'
require 'htauth/cli/digest'
rescue LoadError
path = File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
raise if $:.include?(path)
Expand Down
2 changes: 1 addition & 1 deletion bin/htpasswd-ruby
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby

begin
require 'htauth/cli'
require 'htauth/cli/passwd'
rescue LoadError
path = File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
raise if $:.include?(path)
Expand Down
12 changes: 7 additions & 5 deletions htauth.gemspec
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# DO NOT EDIT - This file is automatically generated
# Make changes to Manifest.txt and/or Rakefile and regenerate
# -*- encoding: utf-8 -*-
# stub: htauth 2.2.0 ruby lib
# stub: htauth 2.3.0 ruby lib

Gem::Specification.new do |s|
s.name = "htauth".freeze
s.version = "2.2.0".freeze
s.version = "2.3.0".freeze

s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
s.metadata = { "bug_tracker_uri" => "https://github.com/copiousfreetime/htauth/issues", "changelog_uri" => "https://github.com/copiousfreetime/htauth/blob/master/HISTORY.md", "homepage_uri" => "https://github.com/copiousfreetime/htauth", "source_code_uri" => "https://github.com/copiousfreetime/htauth" } if s.respond_to? :metadata=
s.require_paths = ["lib".freeze]
s.authors = ["Jeremy Hinegardner".freeze]
s.date = "2024-01-16"
s.description = "HTAuth is a pure ruby replacement for the Apache support programs htdigest and htpasswd. Command line and API access are provided for access to htdigest and htpasswd files.".freeze
s.date = "2024-02-04"
s.description = "HTAuth provides an API and commandline tools for managing Apache/httpd style htpasswd and htdigest files.".freeze
s.email = "[email protected]".freeze
s.executables = ["htdigest-ruby".freeze, "htpasswd-ruby".freeze]
s.extra_rdoc_files = ["CONTRIBUTING.md".freeze, "HISTORY.md".freeze, "Manifest.txt".freeze, "README.md".freeze]
Expand All @@ -22,12 +22,14 @@ Gem::Specification.new do |s|
s.rdoc_options = ["--main".freeze, "README.md".freeze, "--markup".freeze, "tomdoc".freeze]
s.required_ruby_version = Gem::Requirement.new(">= 2.3.0".freeze)
s.rubygems_version = "3.5.3".freeze
s.summary = "HTAuth is a pure ruby replacement for the Apache support programs htdigest and htpasswd. Command line and API access are provided for access to htdigest and htpasswd files.".freeze
s.summary = "HTAuth provides an API and commandline tools for managing Apache/httpd style htpasswd and htdigest files.".freeze
s.test_files = ["spec/algorithm_spec.rb".freeze, "spec/bcrypt_spec.rb".freeze, "spec/cli/digest_spec.rb".freeze, "spec/cli/passwd_spec.rb".freeze, "spec/crypt_spec.rb".freeze, "spec/digest_entry_spec.rb".freeze, "spec/digest_file_spec.rb".freeze, "spec/md5_spec.rb".freeze, "spec/passwd_entry_spec.rb".freeze, "spec/passwd_file_spec.rb".freeze, "spec/plaintext_spec.rb".freeze, "spec/sha1_spec.rb".freeze, "spec/spec_helper.rb".freeze, "spec/test.add.digest".freeze, "spec/test.add.passwd".freeze, "spec/test.delete.digest".freeze, "spec/test.delete.passwd".freeze, "spec/test.original.digest".freeze, "spec/test.original.passwd".freeze, "spec/test.update.digest".freeze, "spec/test.update.passwd".freeze]

s.specification_version = 4

s.add_runtime_dependency(%q<bcrypt>.freeze, ["~> 3.1".freeze])
s.add_runtime_dependency(%q<base64>.freeze, ["~> 0.2".freeze])
s.add_development_dependency(%q<argon2>.freeze, ["~> 2.3".freeze])
s.add_development_dependency(%q<rake>.freeze, ["~> 13.1".freeze])
s.add_development_dependency(%q<minitest>.freeze, ["~> 5.21".freeze])
s.add_development_dependency(%q<minitest-junit>.freeze, ["~> 1.1".freeze])
Expand Down
2 changes: 1 addition & 1 deletion lib/htauth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def self.sub_path( sub, *args )

require 'htauth/version'
require 'htauth/algorithm'
require 'htauth/console'
require 'htauth/argon2'
require 'htauth/bcrypt'
require 'htauth/crypt'
require 'htauth/digest_entry'
Expand Down
14 changes: 13 additions & 1 deletion lib/htauth/algorithm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Algorithm
SALT_CHARS = (%w[ . / ] + ("0".."9").to_a + ('A'..'Z').to_a + ('a'..'z').to_a).freeze
SALT_LENGTH = 8

# Public: flag for the argon2 algorithm
ARGON2 = "argon2".freeze
# Public: flag for the bcrypt algorithm
BCRYPT = "bcrypt".freeze
# Public: flag for the md5 algorithm
Expand Down Expand Up @@ -84,7 +86,17 @@ def secure_compare(a, b)
end

# Internal
def encode(password) ; end
def encode(password)
raise NotImplementedError, "#{self.class.name} must implement #{self.class.name}.encode(password)"
end

# Internal: Does the given password match the digest, the default just
# encodes and secure compares the result, different algorithms may overide
# this method
def verify_password?(password, digest)
encoded = encode(password)
self.class.secure_compare(encoded, digest)
end

# Internal: 8 bytes of random items from SALT_CHARS
def gen_salt(length = SALT_LENGTH)
Expand Down
77 changes: 77 additions & 0 deletions lib/htauth/argon2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
require 'htauth/algorithm'
begin
require 'argon2'
rescue LoadError
end

module HTAuth
# Internal: Support of the argon2id algorithm and password format.

class Argon2 < Algorithm
class NotSupportedError < ::HTAuth::InvalidAlgorithmError
def message
"Unfortunately Argon2 passwords are not supported on `#{RUBY_PLATFORM} at this time. This because the upstream argon2 gem does not support windows."
end
end
class NotInstalledError < ::HTAuth::InvalidAlgorithmError
def message
"Argon2 passwords are supported if the `argon2' gem is installed. Add `gem 'argon2', '~> 2.3'` to your Gemfile"
end
end

# from upstream, used to help make a nice error message if its not installed
# https://github.com/technion/ruby-argon2/blob/3388d7e05e8b486ea4ba8bd2aeb1e9988f025f13/lib/argon2/hash_format.rb#L45
PREFIX = /^\$argon2(id?|d).{,113}/.freeze
ARGON2_GEM_INSTALLED = defined?(::Argon2)

def self.supported?
!::Gem.win_platform?
end

def self.ensure_available!
raise NotSupportedError unless supported?
raise NotInstalledError unless ARGON2_GEM_INSTALLED
end

attr_accessor :options

def self.handles?(password_entry)
return false unless PREFIX.match?(password_entry)
ensure_available!

return ::Argon2::Password.valid_hash?(password_entry)
end

def self.extract_options_from_existing_password_field(existing)
hash_format = ::Argon2::HashFormat.new(existing)

# m_cost on the input is the 2**m_cost, but in the hash its the number of
# bytes, so need to convert it back to a power of 2, which is the
# log2(m_cost)

{
t_cost: hash_format.t_cost,
m_cost: ::Math.log2(hash_format.m_cost).floor,
p_cost: hash_format.p_cost,
}
end

def initialize(params = { profile: :rfc_9106_low_memory })
self.class.ensure_available!
if existing = (params['existing'] || params[:existing]) then
@options = self.class.extract_options_from_existing_password_field(existing)
else
@options = params
end
end

def encode(password)
argon2 = ::Argon2::Password.new(options)
argon2.create(password)
end

def verify_password?(password, digest)
::Argon2::Password.verify_password(password, digest)
end
end
end
5 changes: 5 additions & 0 deletions lib/htauth/bcrypt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,10 @@ def initialize(params = {})
def encode(password)
::BCrypt::Password.create(password, :cost => cost)
end

def verify_password?(password, digest)
bc = ::BCrypt::Password.new(digest)
bc.is_password?(password)
end
end
end
3 changes: 1 addition & 2 deletions lib/htauth/cli.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
require 'htauth'
require 'htauth/console'
module HTAuth
module CLI

end
end
require 'htauth/cli/digest'
require 'htauth/cli/passwd'
5 changes: 1 addition & 4 deletions lib/htauth/cli/digest.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
require 'htauth/version'
require 'htauth/error'
require 'htauth/digest_file'
require 'htauth/console'
require 'htauth/cli'

require 'ostruct'
require 'optparse'
Expand Down
Loading

0 comments on commit e4296e9

Please sign in to comment.