Skip to content

Commit

Permalink
Merge pull request #1 from Pevensie/v1
Browse files Browse the repository at this point in the history
Initial development
  • Loading branch information
isaacharrisholt authored Jul 14, 2024
2 parents f16f0e5 + 9176269 commit f54c092
Show file tree
Hide file tree
Showing 8 changed files with 517 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: test

on:
push:
branches:
- master
- main
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: "26.0.2"
gleam-version: "1.3.2"
rebar3-version: "3"
# elixir-version: "1.15.4"
- run: gleam deps download
- run: gleam test
- run: gleam format --check src test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Argus

Argon2 password hashing library for Gleam, based on the reference C implementation.

[![Package Version](https://img.shields.io/hexpm/v/antigone)](https://hex.pm/packages/antigone)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/antigone/)

This library uses another Pevensie project, [jargon](https://github.com/Pevensie/jargon), to provide the underlying NIF.

It currently only supports Gleam's Erlang backend.

## Example

```bash
gleam add argus
```

```gleam
import argus
pub fn main() {
// Hash a password using the recommended settings for Argon2id.
let assert Ok(hashes) =
argus.hasher()
|> argus.hash("password", gen_salt())
// Hash a password with custom settings and a custom salt.
let assert Ok(hashes) =
argus.hasher()
|> argus.algorithm(argus.Argon2id)
|> argus.time_cost(3)
|> argus.memory_cost(12228) // 12 mebibytes
|> argus.parallelism(1)
|> argus.hash_length(32)
|> argus.hash("password", "custom_salt")
// Verify a password.
let assert Ok(True) = argus.verify(hashes.encoded_hash, "password")
}
```

More information can be found in the [documentation](https://hexdocs.pm/argus/).

## Why 'Argus'?

[Argus](https://en.wikipedia.org/wiki/Argus_(Argonaut)) was the builder of the
[Argo](https://en.wikipedia.org/wiki/Argo) ship and was one of the
[Argonauts](https://en.wikipedia.org/wiki/Argonauts).
13 changes: 13 additions & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name = "argus"
version = "1.0.0"
description = "Argon2 password hashing library for Gleam, based on the reference C implementation."
licences = ["MIT"]
repository = { type = "github", user = "Pevensie", repo = "argus" }
# links = [{ title = "Website", href = "" }]

[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
startest = ">= 0.4.0 and < 1.0.0"
jargon = ">= 1.0.0 and < 2.0.0"

[dev-dependencies]
30 changes: 30 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file was generated by Gleam
# You typically do not need to edit this file

packages = [
{ name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
{ name = "bigben", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "bigben", source = "hex", outer_checksum = "8E5A98FA6E981EEEF016C40F1CDFADA095927CAF6CAAA0C7E295EED02FC95947" },
{ name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
{ name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
{ name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
{ name = "gleam_javascript", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "483631D3001FCE8EB12ADEAD5E1B808440038E96F93DA7A32D326C82F480C0B2" },
{ name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
{ name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
{ name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" },
{ name = "glint", version = "1.0.0-rc2", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "FD5C47CE237CA67121F3946ADE7C630750BB67F5E8A4717D2DF5B5EE758CCFDB" },
{ name = "jargon", version = "1.0.0", build_tools = ["rebar3"], requirements = [], otp_app = "jargon", source = "hex", outer_checksum = "60FBFACC920EAEBC96C76DA3D8ED814FABDDC2103CC0D04FE314A3C15F3174DF" },
{ name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
{ name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
{ name = "startest", version = "0.4.0", build_tools = ["gleam"], requirements = ["argv", "bigben", "birl", "exception", "gleam_community_ansi", "gleam_erlang", "gleam_javascript", "gleam_stdlib", "glint", "simplifile", "tom"], otp_app = "startest", source = "hex", outer_checksum = "BA5B1D896F097040557C7DC311FA3FFACEBBD182CCBB02503D7218545D37F348" },
{ name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
]

[requirements]
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
jargon = { version = ">= 1.0.0 and < 2.0.0"}
startest = { version = ">= 0.4.0 and < 1.0.0" }
182 changes: 182 additions & 0 deletions src/argus.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
pub type Argon2Algorithm {
Argon2d
Argon2i
Argon2id
}

pub opaque type ArgusHasher {
ArgusHasher(
algorithm: Argon2Algorithm,
time_cost: Int,
memory_cost: Int,
parallelism: Int,
hash_length: Int,
)
}

pub type ArgusHash {
ArgusHash(raw_hash: BitArray, encoded_hash: String)
}

pub type HashError {
OutputPointerIsNull
OutputTooShort
OutputTooLong
PasswordTooShort
PasswordTooLong
SaltTooShort
SaltTooLong
AssociatedDataTooShort
AssociatedDataTooLong
SecretTooShort
SecretTooLong
TimeCostTooSmall
TimeCostTooLarge
MemoryCostTooSmall
MemoryCostTooLarge
TooFewLanes
TooManyLanes
PasswordPointerMismatch
SaltPointerMismatch
SecretPointerMismatch
AssociatedDataPointerMismatch
MemoryAllocationError
FreeMemoryCallbackNull
AllocateMemoryCallbackNull
IncorrectParameter
IncorrectType
InvalidAlgorithm
OutputPointerMismatch
TooFewThreads
TooManyThreads
NotEnoughMemory
EncodingFailed
DecodingFailed
ThreadFailure
DecodingLengthFailure
VerificationFailure
UnknownErrorCode
}

/// Create a new hasher with default settings based on the
/// [OWASP recommendations](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id).
///
/// Note: if you change the algorithm to Argon2i, you will need to change the
/// `memory_cost` to 12_228 (12 mebibytes) or less for performance reasons.
///
/// The `hasher_argon2i` function is provided with the recommended settings for
/// Argon2i.
pub fn hasher() -> ArgusHasher {
ArgusHasher(
Argon2id,
2,
// 19 mebibytes
19_456,
1,
32,
)
}

/// Create a new hasher with default settings based on the
/// [OWASP recommendations](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id) for
/// Argon2i.
pub fn hasher_argon2i() -> ArgusHasher {
ArgusHasher(
Argon2i,
3,
// 12 mebibytes
12_228,
1,
32,
)
}

/// Set the algorithm to use for the hasher.
pub fn algorithm(hasher: ArgusHasher, algorithm: Argon2Algorithm) -> ArgusHasher {
ArgusHasher(..hasher, algorithm: algorithm)
}

/// Set the time cost to use for the hasher.
pub fn time_cost(hasher: ArgusHasher, time_cost: Int) -> ArgusHasher {
ArgusHasher(..hasher, time_cost: time_cost)
}

/// Set the memory cost to use for the hasher.
pub fn memory_cost(hasher: ArgusHasher, memory_cost: Int) -> ArgusHasher {
ArgusHasher(..hasher, memory_cost: memory_cost)
}

/// Set the parallelism to use for the hasher.
pub fn parallelism(hasher: ArgusHasher, parallelism: Int) -> ArgusHasher {
ArgusHasher(..hasher, parallelism: parallelism)
}

/// Set the hash length to use for the hasher.
pub fn hash_length(hasher: ArgusHasher, hash_length: Int) -> ArgusHasher {
ArgusHasher(..hasher, hash_length: hash_length)
}

/// Hash a password using the provided hasher.
///
/// ## Examples
///
/// ```gleam
/// import argus
///
/// let assert Ok(hashes) =
/// argus.hasher()
/// |> argus.algorithm(argus.Argon2id)
/// |> argus.time_cost(3)
/// |> argus.memory_cost(12228)
/// |> argus.parallelism(1)
/// |> argus.hash_length(32)
/// |> argus.hash("password", gen_salt())
///
/// let assert Ok(True) = argus.verify(hashes.encoded_hash, "password")
/// ```
pub fn hash(
hasher: ArgusHasher,
password: String,
salt: String,
) -> Result(ArgusHash, HashError) {
let result =
jargon_hash(
password,
salt,
hasher.algorithm,
hasher.time_cost,
hasher.memory_cost,
hasher.parallelism,
hasher.hash_length,
)
case result {
Ok(#(raw_hash, encoded_hash)) -> Ok(ArgusHash(raw_hash, encoded_hash))
Error(error) -> Error(error)
}
}

/// Verify a password using the provided encoded hash.
pub fn verify(encoded_hash: String, password: String) -> Result(Bool, HashError) {
jargon_verify(encoded_hash, password)
}

/// Generate a random salt of at least 64 bytes.
@external(erlang, "argus_nif", "gen_salt")
pub fn gen_salt() -> String

@external(erlang, "argus_nif", "hash")
fn jargon_hash(
password: String,
salt: String,
algorithm: Argon2Algorithm,
time_cost: Int,
memory_cost: Int,
parallelism: Int,
hash_length: Int,
) -> Result(#(BitArray, String), HashError)

@external(erlang, "jargon", "verify")
fn jargon_verify(
encoded_hash: String,
password: String,
) -> Result(Bool, HashError)
25 changes: 25 additions & 0 deletions src/argus_nif.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-module(argus_nif).

-export([hash/7, gen_salt/0]).

hash(Password, Salt, Algorithm, TimeCost, MemoryCost, Parallelism, HashLength) ->
case jargon:hash(Password, Salt, Algorithm, TimeCost, MemoryCost, Parallelism, HashLength)
of
{ok, RawHash, EncodedHash} ->
{ok, {RawHash, EncodedHash}};
{error, Error} ->
{error, Error}
end.

gen_random_int(Min, Max) ->
crypto:strong_rand_bytes(4),
<<Int:32/integer>> = crypto:strong_rand_bytes(4),
Int rem (Max - Min) + Min.

%% Use a min of 64 bytes rather than the default of 32
%% for additional security.
gen_salt() ->
Bytes = gen_random_int(64, 1024),
base64:encode(crypto:strong_rand_bytes(Bytes)).


Loading

0 comments on commit f54c092

Please sign in to comment.