/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Copyright (C) 2016 ScyllaDB * * Modified by ScyllaDB */ /* * This file is part of Scylla. * * Scylla is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Scylla is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Scylla. If not, see . */ #include #include #include #include #include #include "auth.hh" #include "password_authenticator.hh" #include "authenticated_user.hh" #include "cql3/query_processor.hh" #include "log.hh" const sstring auth::password_authenticator::PASSWORD_AUTHENTICATOR_NAME("org.apache.cassandra.auth.PasswordAuthenticator"); // name of the hash column. static const sstring SALTED_HASH = "salted_hash"; static const sstring USER_NAME = "username"; static const sstring DEFAULT_USER_NAME = auth::auth::DEFAULT_SUPERUSER_NAME; static const sstring DEFAULT_USER_PASSWORD = auth::auth::DEFAULT_SUPERUSER_NAME; static const sstring CREDENTIALS_CF = "credentials"; static logging::logger logger("password_authenticator"); auth::password_authenticator::~password_authenticator() {} auth::password_authenticator::password_authenticator() {} // TODO: blowfish // Origin uses Java bcrypt library, i.e. blowfish salt // generation and hashing, which is arguably a "better" // password hash than sha/md5 versions usually available in // crypt_r. Otoh, glibc 2.7+ uses a modified sha512 algo // which should be the same order of safe, so the only // real issue should be salted hash compatibility with // origin if importing system tables from there. // // Since bcrypt/blowfish is _not_ (afaict) not available // as a dev package/lib on most linux distros, we'd have to // copy and compile for example OWL crypto // (http://cvsweb.openwall.com/cgi/cvsweb.cgi/Owl/packages/glibc/crypt_blowfish/) // to be fully bit-compatible. // // Until we decide this is needed, let's just use crypt_r, // and some old-fashioned random salt generation. static constexpr size_t rand_bytes = 16; static sstring hashpw(const sstring& pass, const sstring& salt) { // crypt_data is huge. should this be a thread_local static? auto tmp = std::make_unique(); tmp->initialized = 0; auto res = crypt_r(pass.c_str(), salt.c_str(), tmp.get()); if (res == nullptr) { throw std::system_error(errno, std::system_category()); } return res; } static bool checkpw(const sstring& pass, const sstring& salted_hash) { auto tmp = hashpw(pass, salted_hash); return tmp == salted_hash; } static sstring gensalt() { static sstring prefix; std::random_device rd; std::default_random_engine e1(rd()); std::uniform_int_distribution dist; sstring valid_salt = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"; sstring input(rand_bytes, 0); for (char&c : input) { c = valid_salt[dist(e1) % valid_salt.size()]; } sstring salt; if (!prefix.empty()) { return prefix + salt; } auto tmp = std::make_unique(); tmp->initialized = 0; // Try in order: // blowfish 2011 fix, blowfish, sha512, sha256, md5 for (sstring pfx : { "$2y$", "$2a$", "$6$", "$5$", "$1$" }) { salt = pfx + input; if (crypt_r("fisk", salt.c_str(), tmp.get())) { prefix = pfx; return salt; } } throw std::runtime_error("Could not initialize hashing algorithm"); } static sstring hashpw(const sstring& pass) { return hashpw(pass, gensalt()); } future<> auth::password_authenticator::init() { gensalt(); // do this once to determine usable hashing sstring create_table = sprint( "CREATE TABLE %s.%s (" "%s text," "%s text," // salt + hash + number of rounds "options map,"// for future extensions "PRIMARY KEY(%s)" ") WITH gc_grace_seconds=%d", auth::auth::AUTH_KS, CREDENTIALS_CF, USER_NAME, SALTED_HASH, USER_NAME, 90 * 24 * 60 * 60); // 3 months. return auth::setup_table(CREDENTIALS_CF, create_table).then([this] { // instead of once-timer, just schedule this later auth::schedule_when_up([] { return auth::has_existing_users(CREDENTIALS_CF, DEFAULT_USER_NAME, USER_NAME).then([](bool exists) { if (!exists) { cql3::get_local_query_processor().process(sprint("INSERT INTO %s.%s (%s, %s) VALUES (?, ?) USING TIMESTAMP 0", auth::AUTH_KS, CREDENTIALS_CF, USER_NAME, SALTED_HASH ), db::consistency_level::ONE, {DEFAULT_USER_NAME, hashpw(DEFAULT_USER_PASSWORD)}).then([](auto) { logger.info("Created default user '{}'", DEFAULT_USER_NAME); }); } }); }); }); } db::consistency_level auth::password_authenticator::consistency_for_user(const sstring& username) { if (username == DEFAULT_USER_NAME) { return db::consistency_level::QUORUM; } return db::consistency_level::LOCAL_ONE; } const sstring& auth::password_authenticator::class_name() const { return PASSWORD_AUTHENTICATOR_NAME; } bool auth::password_authenticator::require_authentication() const { return true; } auth::authenticator::option_set auth::password_authenticator::supported_options() const { return option_set::of(); } auth::authenticator::option_set auth::password_authenticator::alterable_options() const { return option_set::of(); } future<::shared_ptr > auth::password_authenticator::authenticate( const credentials_map& credentials) const throw (exceptions::authentication_exception) { if (!credentials.count(USERNAME_KEY)) { throw exceptions::authentication_exception(sprint("Required key '%s' is missing", USERNAME_KEY)); } if (!credentials.count(PASSWORD_KEY)) { throw exceptions::authentication_exception(sprint("Required key '%s' is missing", PASSWORD_KEY)); } auto& username = credentials.at(USERNAME_KEY); auto& password = credentials.at(PASSWORD_KEY); // Here was a thread local, explicit cache of prepared statement. In normal execution this is // fine, but since we in testing set up and tear down system over and over, we'd start using // obsolete prepared statements pretty quickly. // Rely on query processing caching statements instead, and lets assume // that a map lookup string->statement is not gonna kill us much. return futurize_apply([this, username, password] { auto& qp = cql3::get_local_query_processor(); return qp.process(sprint("SELECT %s FROM %s.%s WHERE %s = ?", SALTED_HASH, auth::AUTH_KS, CREDENTIALS_CF, USER_NAME), consistency_for_user(username), {username}, true); }).then_wrapped([=](future<::shared_ptr> f) { try { auto res = f.get0(); if (res->empty() || !checkpw(password, res->one().get_as(SALTED_HASH))) { throw exceptions::authentication_exception("Username and/or password are incorrect"); } return make_ready_future<::shared_ptr>(::make_shared(username)); } catch (std::system_error &) { std::throw_with_nested(exceptions::authentication_exception("Could not verify password")); } catch (exceptions::request_execution_exception& e) { std::throw_with_nested(exceptions::authentication_exception(e.what())); } catch (...) { std::throw_with_nested(exceptions::authentication_exception("authentication failed")); } }); } future<> auth::password_authenticator::create(sstring username, const option_map& options) throw (exceptions::request_validation_exception, exceptions::request_execution_exception) { try { auto password = boost::any_cast(options.at(option::PASSWORD)); auto query = sprint("INSERT INTO %s.%s (%s, %s) VALUES (?, ?)", auth::AUTH_KS, CREDENTIALS_CF, USER_NAME, SALTED_HASH); auto& qp = cql3::get_local_query_processor(); return qp.process(query, consistency_for_user(username), { username, hashpw(password) }).discard_result(); } catch (std::out_of_range&) { throw exceptions::invalid_request_exception("PasswordAuthenticator requires PASSWORD option"); } } future<> auth::password_authenticator::alter(sstring username, const option_map& options) throw (exceptions::request_validation_exception, exceptions::request_execution_exception) { try { auto password = boost::any_cast(options.at(option::PASSWORD)); auto query = sprint("UPDATE %s.%s SET %s = ? WHERE %s = ?", auth::AUTH_KS, CREDENTIALS_CF, SALTED_HASH, USER_NAME); auto& qp = cql3::get_local_query_processor(); return qp.process(query, consistency_for_user(username), { hashpw(password), username }).discard_result(); } catch (std::out_of_range&) { throw exceptions::invalid_request_exception("PasswordAuthenticator requires PASSWORD option"); } } future<> auth::password_authenticator::drop(sstring username) throw (exceptions::request_validation_exception, exceptions::request_execution_exception) { try { auto query = sprint("DELETE FROM %s.%s WHERE %s = ?", auth::AUTH_KS, CREDENTIALS_CF, USER_NAME); auto& qp = cql3::get_local_query_processor(); return qp.process(query, consistency_for_user(username), { username }).discard_result(); } catch (std::out_of_range&) { throw exceptions::invalid_request_exception("PasswordAuthenticator requires PASSWORD option"); } } const auth::resource_ids& auth::password_authenticator::protected_resources() const { static const resource_ids ids({ data_resource(auth::AUTH_KS, CREDENTIALS_CF) }); return ids; } ::shared_ptr auth::password_authenticator::new_sasl_challenge() const { class plain_text_password_challenge: public sasl_challenge { public: plain_text_password_challenge(const password_authenticator& a) : _authenticator(a) {} /** * SASL PLAIN mechanism specifies that credentials are encoded in a * sequence of UTF-8 bytes, delimited by 0 (US-ASCII NUL). * The form is : {code}authzIdauthnIdpassword{code} * authzId is optional, and in fact we don't care about it here as we'll * set the authzId to match the authnId (that is, there is no concept of * a user being authorized to act on behalf of another). * * @param bytes encoded credentials string sent by the client * @return map containing the username/password pairs in the form an IAuthenticator * would expect * @throws javax.security.sasl.SaslException */ bytes evaluate_response(bytes_view client_response) throw (exceptions::authentication_exception) override { logger.debug("Decoding credentials from client token"); sstring username, password; auto b = client_response.crbegin(); auto e = client_response.crend(); auto i = b; while (i != e) { if (*i == 0) { sstring tmp(i.base(), b.base()); if (password.empty()) { password = std::move(tmp); } else if (username.empty()) { username = std::move(tmp); } b = ++i; continue; } ++i; } if (username.empty()) { throw exceptions::authentication_exception("Authentication ID must not be null"); } if (password.empty()) { throw exceptions::authentication_exception("Password must not be null"); } _credentials[USERNAME_KEY] = std::move(username); _credentials[PASSWORD_KEY] = std::move(password); _complete = true; return {}; } bool is_complete() const override { return _complete; } future<::shared_ptr> get_authenticated_user() const throw (exceptions::authentication_exception) override { return _authenticator.authenticate(_credentials); } private: const password_authenticator& _authenticator; credentials_map _credentials; bool _complete = false; }; return ::make_shared(*this); }