diff --git a/.travis.yml b/.travis.yml index 1060e48..f36c61f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,10 +10,6 @@ env: matrix: - LUA="luajit 2.0" - LUA="luajit 2.1" - - LUA="luajit 2.1" - ARGON2_VERSION=20160406 - - LUA="luajit 2.1" - ARGON2_VERSION=20160821 before_install: - bash .ci/setup_argon2.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ded321e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +## [Unreleased][unrelease] + +## [3.0.0] - 2016/12/08 + +**Note**: This module's version was bumped to `3.0.0` to reflect the +interoperability of its API with the lua-argon2 implementation. In the future, +lua-argon2 and lua-argon2-ffi will continue sharing the same version number +for similar versions. + +### Changed + +- :warning: This version is only compatible with Argon2 + [20160406](https://github.com/P-H-C/phc-winner-argon2/releases/tag/20160406) + and later. +- :warning: Renamed the `encrypt()` function to `hash_encoded()`, in order to + carry a stronger meaning and to eventually implement a `hash_raw()` function + in the future. +- New `variants` field with supported Argon2 encoding variants (as userdatum). + See documentation and the "Added" section of this Changelog. +- Updated the default hashing options to match those of the Argon2 CLI: + `t_cost = 3`, `m_cost = 4096`, `parallelism = 1`, `hash_len = 32`. + +### Added + +- :stars: Support for Argon2id encoding variant. + [#24](https://github.com/thibaultcha/lua-argon2/pull/24) +- We now automatically compute the length of the retrieved encoded hash from + `encrypt()`. [#21](https://github.com/thibaultcha/lua-argon2/pull/21) +- New option: `hash_len`. + [#22](https://github.com/thibaultcha/lua-argon2/pull/22) +- Return errors from `verify()`. A mismatch now returns `false, nil`, while an + error will return `nil, "err string"`. + [#23](https://github.com/thibaultcha/lua-argon2/pull/23) + +## [1.0.0] - 2016/04/10 + +### Added + +- :stars: Support for Argon2 + [20160406](https://github.com/P-H-C/phc-winner-argon2/releases/tag/20160406) + (and later). The major version of this module has been bumped because the + resulting hashes will not be backwards compatible. +- Performance improvements. + [e9e6f9](https://github.com/thibaultcha/lua-argon2-ffi/commit/e9e6f9a609f40f9f26834364e05d701fbc0f9780) + +## [0.0.1] - 2016/02/21 + +Initial release of this module for Argon2 +[20151206](https://github.com/P-H-C/phc-winner-argon2/releases/tag/20151206). + +[unreleased]: https://github.com/thibaultcha/lua-argon2-ffi/compare/3.0.0...master +[3.0.0]: https://github.com/thibaultcha/lua-argon2-ffi/compare/1.0.0...3.0.0 +[1.0.0]: https://github.com/thibaultcha/lua-argon2-ffi/compare/0.0.1...1.0.0 +[0.0.1]: https://github.com/thibaultcha/lua-argon2-ffi/compare/a2f94a08ec34bdd570ff707f5e2bebf87a60ba62...0.0.1 diff --git a/README.md b/README.md index dea3dd0..75d944a 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,24 @@ # lua-argon2-ffi + [![Module Version][badge-version-image]][luarocks-argon2-ffi] [![Build Status][badge-travis-image]][badge-travis-url] [![Coverage Status][badge-coveralls-image]][badge-coveralls-url] -FFI binding of [Argon2] for LuaJIT. +LuaJIT FFI binding for the [Argon2] password hashing function. While [lua-argon2] provides a PUC Lua binding through the Lua C API, this module is a binding for the LuaJIT FFI, especially fit for use in [ngx_lua]/[OpenResty]. -### Prerequisites +### Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Documentation](#documentation) +- [Example](#example) +- [License](#license) + +### Requirements The [Argon2] shared library must be compiled and available in your system. @@ -19,11 +28,16 @@ Compatibility: - Version `1.x` of this module is compatible with Argon2 [`20160406`](https://github.com/P-H-C/phc-winner-argon2/releases/tag/20160406) and later. +- Version `3.x` of this module is compatible with Argon2 + [`20161029`](https://github.com/P-H-C/phc-winner-argon2/releases/tag/20161029) + and later. See the [CI builds][badge-coveralls-url] for the status of the currently supported versions. -### Install +[Back to TOC](#table-of-contents) + +### Installation This binding can be installed via [Luarocks](https://luarocks.org): @@ -33,61 +47,85 @@ $ luarocks install argon2-ffi Or simply by copying the `src/argon2.lua` file in your `LUA_PATH`. -### Usage +[Back to TOC](#table-of-contents) + +### Documentation **Note**: lua-argon2-ffi uses the same API as [lua-argon2], to the exception of the default settings capabilities of lua-argon2. -Encrypt: +This binding's documentation is available at +. + +The Argon2 password hashing function documentation is available at +. + +[Back to TOC](#table-of-contents) + +### Example + +Hash a password to an encoded string: ```lua local argon2 = require "argon2" --- Prototype --- local hash, err = argon2.encrypt(pwd, salt, opts) +-- local encoded, err = argon2.hash_encoded(pwd, salt, opts) --- Argon2i -local hash = assert(argon2.encrypt("password", "somesalt")) --- hash is "$argon2i$m=12,t=2,p=1$c29tZXNhbHQ$ltrjNRFqTXmsHj++TFGZxg+zSg8hSrrSJiViCRns1HM" +local encoded = assert(argon2.hash_encoded("password", "somesalt")) +-- encoded is "$argon2i$v=19$m=4096,t=3,p=1$c29tZXNhbHQ$iWh06vD8Fy27wf9npn6FXWiCX4K6pW6Ue1Bnzz07Z8A" --- Argon2d -local hash = assert(argon2.encrypt("password", "somesalt", {argon2d = true})) --- hash is "$argon2d$m=12,t=2,p=1$c29tZXNhbHQ$mfklun4fYCbv2Hw0UnZZ56xAqWbjD+XRMSN9h6SfLe4" +local encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_d +})) +-- encoded is "$argon2d$v=19$m=4096,t=3,p=1$c29tZXNhbHQ$2+JCoQtY/2x5F0VB9pEVP3xBNguWP1T25Ui0PtZuk8o" + +--- Argon2id +local encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_id +})) +-- encoded is "$argon2id$v=19$m=4096,t=3,p=1$c29tZXNhbHQ$qLml5cbqFAO6YxVHhrSBHP0UWdxrIxkNcM8aMX3blzU" -- Hashing options -local hash = assert(argon2.encrypt("password", "somesalt", { +local encoded = assert(argon2.hash_encoded("password", "somesalt", { t_cost = 4, - m_cost = 24, - parallelism = 2, - hash_len = 64 + m_cost = math.pow(2, 16), -- 65536 KiB + parallelism = 2 })) --- hash is "$argon2i$v=19$m=24,t=4,p=2$c29tZXNhbHQ$NV3zeCzIhUhosd7jtTuifTEoPOb/aPAtO0oTYdZkWfNBXCglBgxVEiJy+tLG4j011vZRO3pnmG82Vc/C1B6Tzw" +-- encoded is "$argon2i$v=19$m=65536,t=4,p=2$c29tZXNhbHQ$n6x5DKNWV8BOeKemQJRk7BU3hcaCVomtn9TCyEA0inM" ``` -Verify: +Verify a password against an encoded string: ```lua local argon2 = require "argon2" --- Prototype -- local ok, err = argon2.decrypt(hash, plain) -local hash = assert(argon2.encrypt("password", "somesalt")) --- hash is an argon2i hash +local encoded = assert(argon2.hash_encoded("password", "somesalt")) +-- encoded: argon2i encoded hash -assert(argon2.verify(hash, "password")) -- ok: true -assert(argon2.verify(hash, "passworld")) -- error: The password did not match -``` +local ok, err = argon2.verify(encoded, "password") +if err then + error("could not verify: " .. err) +end -### Documentation +if not ok then + error("The password does not match the supplied hash") +end +``` -This module's API being the same as [lua-argon2]'s, the detailed documentation -is available at . +[Back to TOC](#table-of-contents) ### License Work licensed under the MIT License. Please check -[P-H-C/phc-winner-argon2][Argon2] for license over Argon2 and the reference +[P-H-C/phc-winner-argon2][Argon2] for the license over Argon2 and the reference implementation. +[Back to TOC](#table-of-contents) + [Argon2]: https://github.com/P-H-C/phc-winner-argon2 [lua-argon2]: https://github.com/thibaultCha/lua-argon2 [luarocks-argon2-ffi]: http://luarocks.org/modules/thibaultcha/argon2-ffi @@ -97,6 +135,6 @@ implementation. [badge-travis-url]: https://travis-ci.org/thibaultcha/lua-argon2-ffi [badge-travis-image]: https://travis-ci.org/thibaultcha/lua-argon2-ffi.svg?branch=master -[badge-version-image]: https://img.shields.io/badge/version-1.0.0-blue.svg?style=flat +[badge-version-image]: https://img.shields.io/badge/version-3.0.0-blue.svg?style=flat [badge-coveralls-url]: https://coveralls.io/github/thibaultcha/lua-argon2-ffi?branch=master [badge-coveralls-image]: https://coveralls.io/repos/github/thibaultcha/lua-argon2-ffi/badge.svg?branch=master diff --git a/argon2-ffi-1.0.0-0.rockspec b/argon2-ffi-3.0.0-1.rockspec similarity index 57% rename from argon2-ffi-1.0.0-0.rockspec rename to argon2-ffi-3.0.0-1.rockspec index a77571d..fab793f 100644 --- a/argon2-ffi-1.0.0-0.rockspec +++ b/argon2-ffi-3.0.0-1.rockspec @@ -1,12 +1,12 @@ package = "argon2-ffi" -version = "1.0.0-0" +version = "3.0.0-1" source = { - url = "git://github.com/thibaultCha/lua-argon2-ffi", - tag = "1.0.0" + url = "git://github.com/thibaultcha/lua-argon2-ffi", + tag = "3.0.0" } description = { - summary = "LuaJIT FFI binding for the Argon2 password hashing algorithm", - homepage = "https://github.com/thibaultCha/lua-argon2-ffi", + summary = "LuaJIT FFI binding for the Argon2 password hashing function", + homepage = "https://github.com/thibaultcha/lua-argon2-ffi", license = "MIT" } build = { diff --git a/spec/argon2_spec.lua b/spec/argon2_spec.lua index fec48cc..26e6d6a 100644 --- a/spec/argon2_spec.lua +++ b/spec/argon2_spec.lua @@ -1,147 +1,336 @@ local argon2 = require "argon2" describe("argon2", function() - it("_VERSION field", function() - assert.equal("1.0.0", argon2._VERSION) - end) - it("_AUTHOR field", function() - assert.equal("Thibault Charbonnier", argon2._AUTHOR) - end) - it("_LICENSE field", function() - assert.equal("MIT", argon2._LICENSE) - end) - it("_URL field", function() - assert.equal("https://github.com/thibaultCha/lua-argon2-ffi", argon2._URL) - end) + it("_VERSION field", function() + assert.equal("3.0.0", argon2._VERSION) + end) + + it("_AUTHOR field", function() + assert.equal("Thibault Charbonnier", argon2._AUTHOR) + end) + + it("_LICENSE field", function() + assert.equal("MIT", argon2._LICENSE) + end) + + it("_URL field", function() + assert.equal("https://github.com/thibaultcha/lua-argon2-ffi", argon2._URL) + end) + + it("variants field", function() + assert.is_table(argon2.variants) + --assert.is_userdata(argon2.variants.argon2_i) + --assert.is_userdata(argon2.variants.argon2_d) + --assert.is_userdata(argon2.variants.argon2_id) + end) end) -describe("encrypt()", function() - it("should throw error on invalid argument", function() - assert.has_error(function() - argon2.encrypt(nil) - end, "bad argument #1 to 'encrypt' (string expected, got nil)") - - assert.has_error(function() - argon2.encrypt("", nil) - end, "bad argument #2 to 'encrypt' (string expected, got nil)") - - assert.has_error(function() - argon2.encrypt("", "", "") - end, "bad argument #3 to 'encrypt' (table expected, got string)") - - assert.has_error(function() - argon2.encrypt("", "", {t_cost = ""}) - end, "expected t_cost to be a number") - - assert.has_error(function() - argon2.encrypt("", "", {m_cost = ""}) - end, "expected m_cost to be a number") - - assert.has_error(function() - argon2.encrypt("", "", {parallelism = ""}) - end, "expected parallelism to be a number") - - assert.has_error(function() - argon2.encrypt("", "", {hash_len = ""}) - end, "expected hash_len to be a number") - end) - it("salt too short", function() - local hash, err = argon2.encrypt("password", "") - assert.falsy(hash) - assert.equal("Salt is too short", err) - - hash, err = argon2.encrypt("password", "abcdefg") - assert.falsy(hash) - assert.equal("Salt is too short", err) - end) - it("hash_len too short", function() - local hash, err = argon2.encrypt("password", "somesalt", {hash_len = 2}) - assert.falsy(hash) - assert.equal("Output is too short", err) - end) - it("should return a hash", function() - local hash = assert(argon2.encrypt("password", "somesalt")) - assert.matches("$argon2i$v=19$m=12,t=2,p=1$", hash, nil, true) - local hash_pattern = "%$" .. string.rep(".", 43) .. "$" - assert.matches(hash_pattern, hash) - end) - it("should return a hash with longer encoded_len", function() - local hash = assert(argon2.encrypt("password", string.rep("salt", 10))) - assert.matches("$argon2i$v=19$m=12,t=2,p=1$", hash, nil, true) - end) - it("should hash with argon2d", function() - local hash = assert(argon2.encrypt("password", "somesalt", {argon2d = true})) - assert.matches("argon2d", hash) - end) - it("should accept time cost", function() - local hash = assert(argon2.encrypt("password", "somesalt", {t_cost = 4})) - assert.matches("t=4", hash) - end) - it("should accept memory cost", function() - local hash = assert(argon2.encrypt("password", "somesalt", {m_cost = 13})) - assert.matches("m=13", hash) - end) - it("should accept parallelism", function() - local hash = assert(argon2.encrypt("password", "somesalt", {parallelism = 2, m_cost = 24})) - assert.matches("p=2", hash) - end) - it("should accept hash_len", function() - local hash = assert(argon2.encrypt("password", "somesalt", {hash_len = 7})) - local hash_pattern = "%$" .. string.rep(".", 10) .. "$" - assert.matches(hash_pattern, hash) - end) - it("should accept all options", function() - local hash = assert(argon2.encrypt("password", "somesalt", { - t_cost = 4, - m_cost = 24, - parallelism = 2, - hash_len = 9 - })) - assert.matches("m=24,t=4,p=2", hash) - local hash_pattern = "%$" .. string.rep(".", 12) .. "$" - assert.matches(hash_pattern, hash) - end) +describe("hash_encoded()", function() + + describe("variants encoding", function() + it("argon2_i", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt")) + assert.matches("$argon2i$v=19$m=4096,t=3,p=1$", encoded, nil, true) + end) + + it("argon2_d", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_d + })) + assert.matches("$argon2d$", encoded, nil, true) + end) + + it("argon2_id", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_id + })) + assert.matches("$argon2id$", encoded, nil, true) + end) + end) + + it("calculates the appropriate encoded_len (triggered by long salt)", function() + local encoded = assert(argon2.hash_encoded("password", string.rep("salt", 8))) + assert.matches("$argon2i$v=19$m=4096,t=3,p=1$", encoded, nil, true) + end) + + describe("errors", function() + it("throws error on invalid argument", function() + assert.has_error(function() + argon2.hash_encoded(nil) + end, "bad argument #1 to 'hash_encoded' (string expected, got nil)") + + assert.has_error(function() + argon2.hash_encoded("", nil) + end, "bad argument #2 to 'hash_encoded' (string expected, got nil)") + + assert.has_error(function() + argon2.hash_encoded("", "", "") + end, "bad argument #3 to 'hash_encoded' (expected to be a table)") + + assert.has_error(function() + argon2.hash_encoded("", "", {t_cost = ""}) + end, "bad argument #3 to 'hash_encoded' (expected t_cost to be a number, got string)") + + assert.has_error(function() + argon2.hash_encoded("", "", {m_cost = ""}) + end, "bad argument #3 to 'hash_encoded' (expected m_cost to be a number, got string)") + + assert.has_error(function() + argon2.hash_encoded("", "", {parallelism = ""}) + end, "bad argument #3 to 'hash_encoded' (expected parallelism to be a number, got string)") + + --[[ + assert.has_error(function() + argon2.hash_encoded("", "", {}, "") + end, "expecting no more than 3 arguments, but got 4") + --]] + + assert.has_error(function() + argon2.hash_encoded("", "", { variant = "" }) + end, "bad argument #3 to 'hash_encoded' (expected variant to be an argon2_type, got string)") + end) + + it("returns error on failure to hash_encoded", function() + local encoded, err = argon2.hash_encoded("password", "") + assert.falsy(encoded) + assert.equal("Salt is too short", err) + + encoded, err = argon2.hash_encoded("password", "abcdefg") + assert.falsy(encoded) + assert.equal("Salt is too short", err) + end) + end) + + describe("options", function() + it("accepts time cost", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + t_cost = 4 + })) + assert.matches("t=4", encoded) + end) + + it("accepts memory cost", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + m_cost = 13 + })) + assert.matches("m=13", encoded) + end) + + it("accepts parallelism", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + parallelism = 2, + m_cost = 24 + })) + assert.matches("p=2", encoded) + end) + + it("accepts hash_len", function() + local encoded_hash_default = assert(argon2.hash_encoded("password", "somesalt")) + assert.is_string(encoded_hash_default) + + local encoded_hash_64 = assert(argon2.hash_encoded("password", "somesalt", { + hash_len = 64 + })) + assert.is_string(encoded_hash_64) + assert.not_equal(encoded_hash_default, encoded_hash_64) + end) + + it("accepts several options at once", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + t_cost = 4, + m_cost = 24, + parallelism = 2 + })) + assert.matches("m=24,t=4,p=2", encoded) + end) + end) end) describe("verify()", function() - it("should throw error on invalid argument", function() - assert.has_error(function() - argon2.verify(nil) - end, "bad argument #1 to 'verify' (string expected, got nil)") - - assert.has_error(function() - argon2.verify("", nil) - end, "bad argument #2 to 'verify' (string expected, got nil)") - end) - it("should verify ok", function() - local hash = assert(argon2.encrypt("password", "somesalt")) - assert(argon2.verify(hash, "password")) - end) - it("should verify ok with hash_len option", function() - local hash = assert(argon2.encrypt("password", "somesalt", {hash_len = 10})) - assert(argon2.verify(hash, "password")) - end) - it("should verify fail", function() - local hash = assert(argon2.encrypt("password", "somesalt")) - local ok, err = argon2.verify(hash, "passworld") - assert.False(ok) - assert.equal("The password did not match.", err) - end) - it("should verify argon2d ok", function() - local hash = assert(argon2.encrypt("password", "somesalt", {argon2d = true})) - assert(argon2.verify(hash, "password")) - end) - it("should verify argon2d ok with hash_len option", function() - local hash = assert(argon2.encrypt("password", "somesalt", { - argon2d = true, - hash_len = 10 - })) - assert(argon2.verify(hash, "password")) - end) - it("should verify argon2d fail", function() - local hash = assert(argon2.encrypt("password", "somesalt", {argon2d = true})) - local ok, err = argon2.verify(hash, "passworld") - assert.False(ok) - assert.equal("The password did not match.", err) - end) + + describe("variants", function() + + describe("match returns true", function() + it("argon2_i", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt")) + assert(argon2.verify(encoded, "password")) + end) + + it("argon2_d", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_d + })) + assert(argon2.verify(encoded, "password")) + end) + + it("argon2_id", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_id + })) + assert(argon2.verify(encoded, "password")) + end) + end) + + describe("mismatch returns false and no error", function() + it("argon2_i", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt")) + local ok, err = argon2.verify(encoded, "passworld") + assert.False(ok) + assert.is_nil(err) + end) + + it("argon2_d", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_d + })) + + local ok, err = argon2.verify(encoded, "passworld") + assert.False(ok) + assert.is_nil(err) + end) + + it("argon2_id", function() + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_id + })) + + local ok, err = argon2.verify(encoded, "passworld") + assert.False(ok) + assert.is_nil(err) + end) + end) + end) + + describe("errors", function() + it("throws error on invalid argument", function() + --[[ + assert.has_error(function() + argon2.verify(nil) + end, "expecting 2 arguments, but got 1") + --]] + + assert.has_error(function() + argon2.verify(nil, nil) + end, "bad argument #1 to 'verify' (string expected, got nil)") + + assert.has_error(function() + argon2.verify("", nil) + end, "bad argument #2 to 'verify' (string expected, got nil)") + + --[[ + assert.has_error(function() + argon2.verify("", "", "") + end, "expecting 2 arguments, but got 3") + --]] + end) + + it("returns nil and an error message on failure to decode", function() + local encoded = "$argon2i$v=19$m=4096,t=3,p=1c29tZXNhbHQ$iWh06vD8Fy27wf9npn6FXWiCX4K6pW6Ue1Bnzz07Z8A" + local ok, err = argon2.verify(encoded, "") + assert.is_nil(ok) + assert.equal("Decoding failed", err) + end) + end) +end) + +--[[ +pending("module settings", function() + + it("throws an error on invalid argument", function() + assert.has_error(function() + argon2.t_cost(0, 0) + end, "expecting no more than 1 arguments, but got 2") + + assert.has_error(function() + argon2.t_cost "" + end, "bad argument #1 to 't_cost' (expected t_cost to be a number, got string)") + + assert.has_error(function() + argon2.variant(nil) + end, "bad argument #1 to 'variant' (userdata expected, got nil)") + end) + + it("accepts t_cost module setting", function() + finally(function() + argon2.t_cost(3) + end) + + assert.equal(4, argon2.t_cost(4)) + + local encoded = assert(argon2.hash_encoded("password", "somesalt")) + assert.matches("$argon2i$v=19$m=4096,t=4,p=1$", encoded, nil, true) + + encoded = assert(argon2.hash_encoded("password", "somesalt", { + t_cost = 2 + })) + assert.matches("$argon2i$v=19$m=4096,t=2,p=1$", encoded, nil, true) + end) + + it("accepts m_cost module setting", function() + finally(function() + argon2.m_cost(4096) + end) + + assert.equal(24, argon2.m_cost(24)) + + local encoded = assert(argon2.hash_encoded("password", "somesalt")) + assert.matches("$argon2i$v=19$m=24,t=3,p=1$", encoded, nil, true) + + encoded = assert(argon2.hash_encoded("password", "somesalt", { + m_cost = 12 + })) + assert.matches("$argon2i$v=19$m=12,t=3,p=1$", encoded, nil, true) + end) + + it("accepts parallelism module setting", function() + finally(function() + argon2.parallelism(1) + end) + + assert.equal(2, argon2.parallelism(2)) + + local encoded = assert(argon2.hash_encoded("password", "somesalt", { + m_cost = 24 + })) + assert.matches("$argon2i$v=19$m=24,t=3,p=2$", encoded, nil, true) + + encoded = assert(argon2.hash_encoded("password", "somesalt", { + parallelism = 1 + })) + assert.matches("$argon2i$v=19$m=4096,t=3,p=1$", encoded, nil, true) + end) + + it("accepts hash_len module setting", function() + finally(function() + argon2.hash_len(32) + end) + + local encoded_default_hash_len = assert(argon2.hash_encoded("password", "somesalt")) + + assert.equal(64, argon2.hash_len(64)) + + local encoded_hash_len_64 = assert(argon2.hash_encoded("password", "somesalt")) + + assert.not_equal(encoded_default_hash_len, encoded_hash_len_64) + end) + + it("accepts variant module setting", function() + finally(function() + argon2.variant(argon2.variants.argon2_i) + end) + + local variant = argon2.variant(argon2.variants.argon2_d) + assert.equal(argon2.variants.argon2_d, variant) + + local encoded = assert(argon2.hash_encoded("password", "somesalt")) + assert.matches("$argon2d$v=19$m=4096,t=3,p=1$", encoded, nil, true) + + encoded = assert(argon2.hash_encoded("password", "somesalt", { + variant = argon2.variants.argon2_i + })) + assert.matches("$argon2i$v=19$m=4096,t=3,p=1$", encoded, nil, true) + end) end) +--]] + +-- vim:st=4 sts=4 sw=4 et: diff --git a/src/argon2.lua b/src/argon2.lua index 5390666..ce8ff62 100644 --- a/src/argon2.lua +++ b/src/argon2.lua @@ -2,6 +2,7 @@ local ffi = require "ffi" +local ffi_new = ffi.new local ffi_str = ffi.string local find = string.find local error = error @@ -10,75 +11,109 @@ local type = type ffi.cdef [[ - typedef enum Argon2_type { Argon2_d = 0, Argon2_i = 1 } argon2_type; + typedef enum Argon2_type { + Argon2_d = 0, + Argon2_i = 1, + Argon2_id = 2 + } argon2_type; + + const char *argon2_error_message(int error_code); + + size_t argon2_encodedlen(uint32_t t_cost, uint32_t m_cost, + uint32_t parallelism, uint32_t saltlen, + uint32_t hashlen, argon2_type type); int argon2i_hash_encoded(const uint32_t t_cost, - const uint32_t m_cost, - const uint32_t parallelism, - const void *pwd, const size_t pwdlen, - const void *salt, const size_t saltlen, - const size_t hashlen, char *encoded, - const size_t encodedlen); + const uint32_t m_cost, + const uint32_t parallelism, + const void *pwd, const size_t pwdlen, + const void *salt, const size_t saltlen, + const size_t hashlen, char *encoded, + const size_t encodedlen); int argon2d_hash_encoded(const uint32_t t_cost, - const uint32_t m_cost, - const uint32_t parallelism, - const void *pwd, const size_t pwdlen, - const void *salt, const size_t saltlen, - const size_t hashlen, char *encoded, - const size_t encodedlen); + const uint32_t m_cost, + const uint32_t parallelism, + const void *pwd, const size_t pwdlen, + const void *salt, const size_t saltlen, + const size_t hashlen, char *encoded, + const size_t encodedlen); + + int argon2id_hash_encoded(const uint32_t t_cost, + const uint32_t m_cost, + const uint32_t parallelism, + const void *pwd, const size_t pwdlen, + const void *salt, const size_t saltlen, + const size_t hashlen, char *encoded, + const size_t encodedlen); int argon2_verify(const char *encoded, - const void *pwd, - const size_t pwdlen, - argon2_type type); + const void *pwd, + const size_t pwdlen, + argon2_type type); +]] - const char *argon2_error_message(int error_code); - size_t argon2_encodedlen(uint32_t t_cost, uint32_t m_cost, - uint32_t parallelism, uint32_t saltlen, - uint32_t hashlen, argon2_type type); -]] +local lib = ffi.load "argon2" -local OPTIONS = { - t_cost = 2, - m_cost = 12, - parallelism = 1, - hash_len = 32, - argon2d = false, -} +local ARGON2_OK = 0 +local ARGON2_VERIFY_MISMATCH = -35 -local argon2_t = ffi.typeof(ffi.new "argon2_type") -local c_type_i = ffi.new(argon2_t, "Argon2_i") -local c_type_d = ffi.new(argon2_t, "Argon2_d") +local argon2_i, argon2_d, argon2_id -local lib = ffi.load "argon2" +do + local argon2_t = ffi.typeof(ffi.new "argon2_type") + + argon2_i = ffi_new(argon2_t, "Argon2_i") + argon2_d = ffi_new(argon2_t, "Argon2_d") + argon2_id = ffi_new(argon2_t, "Argon2_id") +end + + +local OPTIONS = { + t_cost = 3, + m_cost = 4096, + parallelism = 1, + hash_len = 32, + variant = argon2_i, +} local _M = { - _VERSION = "1.0.0", + _VERSION = "3.0.0", _AUTHOR = "Thibault Charbonnier", _LICENSE = "MIT", - _URL = "https://github.com/thibaultCha/lua-argon2-ffi", + _URL = "https://github.com/thibaultcha/lua-argon2-ffi", + variants = { + argon2_i = argon2_i, + argon2_d = argon2_d, + argon2_id = argon2_id, + }, } -function _M.encrypt(pwd, salt, opts) + +function _M.hash_encoded(pwd, salt, opts) if type(pwd) ~= "string" then - return error("bad argument #1 to 'encrypt' (string expected, got " .. type(pwd) .. ")", 2) + return error("bad argument #1 to 'hash_encoded' (string expected, got " + .. type(pwd) .. ")", 2) + end + + if type(salt) ~= "string" then + return error("bad argument #2 to 'hash_encoded' (string expected, got " + .. type(salt) .. ")", 2) + end - elseif type(salt) ~= "string" then - return error("bad argument #2 to 'encrypt' (string expected, got " .. type(salt) .. ")", 2) + if opts and type(opts) ~= "table" then + return error("bad argument #3 to 'hash_encoded' (expected to be a " + .. "table)", 2) end if opts == nil then opts = OPTIONS - elseif type(opts) ~= "table" then - return error("bad argument #3 to 'encrypt' (table expected, got " .. type(opts) .. ")", 2) - else for k, v in pairs(OPTIONS) do local o = opts[k] @@ -86,27 +121,46 @@ function _M.encrypt(pwd, salt, opts) if o == nil then opts[k] = v - elseif k ~= "argon2d" and type(o) ~= "number" then - return error("expected " .. k .. " to be a number", 2) + elseif k == "variant" then + if type(o) ~= "cdata" then + return error("bad argument #3 to 'hash_encoded' (expected " + .. k .. " to be an argon2_type, got " + .. type(o) .. ")", 2) + end + + elseif type(o) ~= "number" then + return error("bad argument #3 to 'hash_encoded' (expected " + .. k .. " to be a number, got " + .. type(o) .. ")", 2) end end end - local c_type = opts.argon2d and c_type_d or c_type_i - local buf_len = lib.argon2_encodedlen(opts.t_cost, opts.m_cost, opts.parallelism, - #salt, opts.hash_len, c_type) - local buf = ffi.new("char[?]", buf_len) - local res - if opts.argon2d then - res = lib.argon2d_hash_encoded(opts.t_cost, opts.m_cost, opts.parallelism, - pwd, #pwd, salt, #salt, opts.hash_len, buf, buf_len) + local buf_len = lib.argon2_encodedlen(opts.t_cost, opts.m_cost, + opts.parallelism, #salt, + opts.hash_len, opts.variant) + + local buf = ffi_new("char[?]", buf_len) + local ret_code + + if opts.variant == argon2_d then + ret_code = lib.argon2d_hash_encoded(opts.t_cost, opts.m_cost, + opts.parallelism, pwd, #pwd, salt, + #salt, opts.hash_len, buf, buf_len) + + elseif opts.variant == argon2_id then + ret_code = lib.argon2id_hash_encoded(opts.t_cost, opts.m_cost, + opts.parallelism, pwd, #pwd, salt, + #salt, opts.hash_len, buf, buf_len) + else - res = lib.argon2i_hash_encoded(opts.t_cost, opts.m_cost, opts.parallelism, - pwd, #pwd, salt, #salt, opts.hash_len, buf, buf_len) + ret_code = lib.argon2i_hash_encoded(opts.t_cost, opts.m_cost, + opts.parallelism, pwd, #pwd, salt, + #salt, opts.hash_len, buf, buf_len) end - if res ~= 0 then - local c_msg = lib.argon2_error_message(res) + if ret_code ~= ARGON2_OK then + local c_msg = lib.argon2_error_message(ret_code) return nil, ffi_str(c_msg) end @@ -114,21 +168,38 @@ function _M.encrypt(pwd, salt, opts) end -function _M.verify(hash, plain) - if type(hash) ~= "string" then - return error("bad argument #1 to 'verify' (string expected, got " .. type(hash) .. ")", 2) +function _M.verify(encoded, plain) + if type(encoded) ~= "string" then + return error("bad argument #1 to 'verify' (string expected, got " + .. type(encoded) .. ")", 2) + end - elseif type(plain) ~= "string" then - return error("bad argument #2 to 'verify' (string expected, got " .. type(plain) .. ")", 2) + if type(plain) ~= "string" then + return error("bad argument #2 to 'verify' (string expected, got " + .. type(plain) .. ")", 2) end - local argon2d = find(hash, "argon2d", nil, true) ~= nil - local c_type = argon2d and c_type_d or c_type_i + local variant - local res = lib.argon2_verify(hash, plain, #plain, c_type) + if find(encoded, "argon2d", nil, true) then + variant = argon2_d - if res ~= 0 then - return false, "The password did not match." + elseif find(encoded, "argon2id", nil, true) then + variant = argon2_id + + else + variant = argon2_i + end + + local ret_code = lib.argon2_verify(encoded, plain, #plain, variant) + + if ret_code == ARGON2_VERIFY_MISMATCH then + return false + end + + if ret_code ~= ARGON2_OK then + local c_msg = lib.argon2_error_message(ret_code) + return nil, ffi_str(c_msg) end return true