local type = type local fmt = string.format local find = string.find local pcall = pcall local remove = table.remove local concat = table.concat local assert = assert local tonumber = tonumber local encode_base64 = ngx.encode_base64 local decode_base64 = ngx.decode_base64 local strip = require("kong.tools.string").strip local splitn = require("kong.tools.string").splitn local get_rand_bytes = require("kong.tools.rand").get_rand_bytes local ENABLED_ALGORITHMS = { ARGON2 = false, BCRYPT = false, PBKDF2 = true, SCRYPT = false, } --local CORES --do -- local infos = utils.get_system_infos() -- if type(infos) == "table" then -- CORES = infos.cores -- end -- if not CORES then -- CORES = ngx.worker.count() or 1 -- end --end local function infer(value) value = strip(value) return tonumber(value, 10) or value end local function parse_phc(phc) local parts, count = splitn(phc, "$", 6) if count < 2 or count > 5 then return nil, "invalid phc string format" end local id = parts[2] local id_parts, id_count = splitn(id, "-") local prefix local digest if id_count == 1 then prefix = id_parts[1] else prefix = id_parts[1] remove(id_parts, 1) digest = concat(id_parts, "-") end local params = {} local prms = parts[3] if prms then local prm_parts, prm_parts_count = splitn(prms, ",") for i = 1, prm_parts_count do local param = prm_parts[i] local kv, kv_count = splitn(param, "=", 3) if kv_count == 1 then params[#params + 1] = infer(kv[1]) elseif kv_count == 2 then local k = strip(kv[1]) params[k] = infer(kv[2]) else return nil, "invalid phc string format for parameter" end end end local salt = parts[4] if salt then local decoded_salt = decode_base64(salt) if decoded_salt then salt = decoded_salt end end local hash = parts[5] if hash then local decoded_hash = decode_base64(hash) if decoded_hash then hash = decoded_hash end end return { id = strip(id), prefix = strip(prefix), digest = strip(digest), params = params, salt = salt, hash = hash, } end local PREFIX = nil -- currently chosen algorithm (nil means that we try to find one) local ARGON2 local ARGON2_ID = "$argon2" if ENABLED_ALGORITHMS.ARGON2 then local ARGON2_PREFIX local ok, crypt = pcall(function() local argon2 = require "argon2" -- argon2 settings local ARGON2_VARIANT = argon2.variants.argon2_id local ARGON2_PARALLELISM = 1 --CORES local ARGON2_T_COST = 1 local ARGON2_M_COST = 4096 local ARGON2_HASH_LEN = 32 local ARGON2_SALT_LEN = 16 local ARGON2_OPTIONS = { variant = ARGON2_VARIANT, parallelism = ARGON2_PARALLELISM, hash_len = ARGON2_HASH_LEN, t_cost = ARGON2_T_COST, m_cost = ARGON2_M_COST, } do local hash = argon2.hash_encoded("", get_rand_bytes(ARGON2_SALT_LEN), ARGON2_OPTIONS) local parts = splitn(hash, "$") remove(parts) remove(parts) ARGON2_PREFIX = concat(parts, "$") end local crypt = {} function crypt.hash(secret) return argon2.hash_encoded(secret, get_rand_bytes(ARGON2_SALT_LEN), ARGON2_OPTIONS) end function crypt.verify(secret, hash) return argon2.verify(hash, secret) end return crypt end) if ok then ARGON2 = crypt PREFIX = PREFIX or ARGON2_PREFIX end end local BCRYPT local BCRYPT_ID = "$2" if ENABLED_ALGORITHMS.BCRYPT then local BCRYPT_PREFIX local ok, crypt = pcall(function() local bcrypt = require "bcrypt" -- bcrypt settings local BCRYPT_ROUNDS = 12 do local hash = bcrypt.digest("", BCRYPT_ROUNDS) local parts = splitn(hash, "$") remove(parts) BCRYPT_PREFIX = concat(parts, "$") end local crypt = {} function crypt.hash(secret) return bcrypt.digest(secret, BCRYPT_ROUNDS) end function crypt.verify(secret, hash) return bcrypt.verify(secret, hash) end return crypt end) if ok then BCRYPT = crypt PREFIX = PREFIX or BCRYPT_PREFIX end end local PBKDF2 local PBKDF2_ID = "$pbkdf2" if ENABLED_ALGORITHMS.PBKDF2 then local PBKDF2_PREFIX local ok, crypt = pcall(function() local openssl_kdf = require "resty.openssl.kdf" -- pbkdf2 default settings local PBKDF2_DIGEST = "sha512" local PBKDF2_ITERATIONS = 10000 local PBKDF2_HASH_LEN = 32 local PBKDF2_SALT_LEN = 16 local EMPTY = {} local kdf local function derive(secret, opts) opts = opts or EMPTY local err if kdf then local _, err = kdf:reset() if err then kdf = nil end end if not kdf then kdf, err = openssl_kdf.new("PBKDF2") if err then return nil, err end end local salt = opts.salt or get_rand_bytes(PBKDF2_SALT_LEN) local hash, err = kdf:derive(opts.outlen or PBKDF2_HASH_LEN, { pass = secret, salt = salt, digest = opts.digest or PBKDF2_DIGEST, iter = opts.iter or PBKDF2_ITERATIONS, }, 4) if not hash then return nil, err end local HASH = encode_base64(hash, true) local SALT = encode_base64(salt, true) return fmt("%s-%s$i=%u,l=%u$%s$%s", PBKDF2_ID, PBKDF2_DIGEST, PBKDF2_ITERATIONS, PBKDF2_HASH_LEN, SALT, HASH) end do local hash = derive("") local parts = splitn(hash, "$") remove(parts) remove(parts) PBKDF2_PREFIX = concat(parts, "$") end local crypt = {} function crypt.hash(secret, options) return derive(secret, options) end function crypt.verify(secret, hash) local phc, err = parse_phc(hash) if not phc then return nil, err end local outlen = phc.params.l if not outlen and phc.hash then outlen = #phc.hash end local calculated_hash, err = derive(secret, { outlen = outlen, salt = phc.salt, digest = phc.digest, iter = phc.params.i }) if not calculated_hash then return nil, err end return calculated_hash == hash end return crypt end) if ok then PBKDF2 = crypt PREFIX = PREFIX or PBKDF2_PREFIX end end local crypt = {} function crypt.hash(secret, options) assert(type(secret) == "string", "secret needs to be a string") if ARGON2 then return ARGON2.hash(secret) end if BCRYPT then return BCRYPT.hash(secret) end if PBKDF2 then return PBKDF2.hash(secret, options) end return nil, "no suitable password hashing algorithm found" end function crypt.verify(secret, hash) if type(secret) ~= "string" then return false, "secret needs to be a string" end if type(hash) ~= "string" then return false, "hash needs to be a string" end if ARGON2 and find(hash, ARGON2_ID, 1, true) == 1 then return ARGON2.verify(secret, hash) end if BCRYPT and find(hash, BCRYPT_ID, 1, true) == 1 then return BCRYPT.verify(secret, hash) end if PBKDF2 and find(hash, PBKDF2_ID, 1, true) == 1 then return PBKDF2.verify(secret, hash) end return false, "no suitable password hashing algorithm found" end function crypt.needs_rehash(hash) if type(hash) ~= "string" then return true end if PREFIX then return find(hash, PREFIX, 1, true) ~= 1 end return true end return crypt