From eef532ca35e2f63b63e1723769b74523cbdc1619 Mon Sep 17 00:00:00 2001 From: Robert Newson Date: Wed, 29 Oct 2014 17:39:21 +0000 Subject: [PATCH] Implement two factor authentication If enabled, require a second factor to acquire a session cookie and reject basic authentication attempts (as second factor cannot be presented). Allow previous and next token for clock skew. --- src/couch_hotp.erl | 40 ++++++++++++++++++++++++++++ src/couch_httpd_auth.erl | 54 ++++++++++++++++++++++++++++++++++++++ src/couch_totp.erl | 23 ++++++++++++++++ test/couch_hotp_tests.erl | 28 ++++++++++++++++++++ test/couch_totp_tests.erl | 55 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 src/couch_hotp.erl create mode 100644 src/couch_totp.erl create mode 100644 test/couch_hotp_tests.erl create mode 100644 test/couch_totp_tests.erl diff --git a/src/couch_hotp.erl b/src/couch_hotp.erl new file mode 100644 index 00000000..896c52af --- /dev/null +++ b/src/couch_hotp.erl @@ -0,0 +1,40 @@ +% Licensed 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. + +-module(couch_hotp). + +-export([generate/4]). + +generate(Alg, Key, Counter, OutputLen) + when is_atom(Alg), is_binary(Key), is_integer(Counter), is_integer(OutputLen) -> + Hmac = hmac(Alg, Key, <>), + Offset = binary:last(Hmac) band 16#f, + Code = + ((binary:at(Hmac, Offset) band 16#7f) bsl 24) + + ((binary:at(Hmac, Offset + 1) band 16#ff) bsl 16) + + ((binary:at(Hmac, Offset + 2) band 16#ff) bsl 8) + + ((binary:at(Hmac, Offset + 3) band 16#ff)), + case OutputLen of + 6 -> Code rem 1000000; + 7 -> Code rem 10000000; + 8 -> Code rem 100000000 + end. + +hmac(Alg, Key, Data) -> + case {Alg, erlang:function_exported(crypto, hmac, 3)} of + {_, true} -> + crypto:hmac(Alg, Key, Data); + {sha, false} -> + crypto:sha_mac(Key, Data); + {Alg, false} -> + throw({unsupported, Alg}) + end. diff --git a/src/couch_httpd_auth.erl b/src/couch_httpd_auth.erl index 7c55a2be..cda51c54 100644 --- a/src/couch_httpd_auth.erl +++ b/src/couch_httpd_auth.erl @@ -23,6 +23,8 @@ -import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]). +-compile({no_auto_import,[integer_to_binary/1]}). + special_test_authentication_handler(Req) -> case header_value(Req, "WWW-Authenticate") of "X-Couch-Test-Auth " ++ NamePass -> @@ -75,6 +77,7 @@ default_authentication_handler(Req, AuthModule) -> nil -> throw({unauthorized, <<"Name or password is incorrect.">>}); UserProps -> + reject_if_totp(UserProps), UserName = ?l2b(User), Password = ?l2b(Pass), case authenticate(Password, UserProps) of @@ -287,6 +290,7 @@ handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req, AuthModule) -> end, case authenticate(Password, UserProps) of true -> + verify_totp(UserProps, Form), UserProps2 = maybe_upgrade_password_hash(UserName, Password, UserProps, AuthModule), % setup the session cookie Secret = ?l2b(ensure_cookie_auth_secret()), @@ -430,3 +434,53 @@ max_age() -> config:get("couch_httpd_auth", "timeout", "600")), [{max_age, Timeout}] end. + +reject_if_totp(User) -> + case get_totp_config(User) of + undefined -> + ok; + _ -> + throw({unauthorized, <<"Name or password is incorrect.">>}) + end. + +verify_totp(User, Form) -> + case get_totp_config(User) of + undefined -> + ok; + {Props} -> + Key = couch_util:get_value(<<"key">>, Props), + Alg = couch_util:to_existing_atom( + couch_util:get_value(<<"algorithm">>, Props, <<"sha">>)), + Len = couch_util:get_value(<<"length">>, Props, 6), + Token = ?l2b(couch_util:get_value("token", Form, "")), + verify_token(Alg, Key, Len, Token) + end. + +get_totp_config(User) -> + couch_util:get_value(<<"totp">>, User). + +verify_token(Alg, Key, Len, Token) -> + Now = make_cookie_time(), + Tokens = [generate_token(Alg, Key, Len, Now - 30), + generate_token(Alg, Key, Len, Now), + generate_token(Alg, Key, Len, Now + 30)], + %% evaluate all tokens in constant time + Match = lists:foldl(fun(T, Acc) -> couch_util:verify(T, Token) or Acc end, + false, Tokens), + case Match of + true -> + ok; + _ -> + throw({unauthorized, <<"Name or password is incorrect.">>}) + end. + +generate_token(Alg, Key, Len, Timestamp) -> + integer_to_binary(couch_totp:generate(Alg, Key, Timestamp, 30, Len)). + +integer_to_binary(Int) when is_integer(Int) -> + case erlang:function_exported(erlang, integer_to_binary, 1) of + true -> + erlang:integer_to_binary(Int); + false -> + ?l2b(integer_to_list(Int)) + end. diff --git a/src/couch_totp.erl b/src/couch_totp.erl new file mode 100644 index 00000000..56e70d81 --- /dev/null +++ b/src/couch_totp.erl @@ -0,0 +1,23 @@ +% Licensed 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. + +-module(couch_totp). + +-export([generate/5]). + +generate(Alg, Key, CounterSecs, StepSecs, OutputLen) + when is_atom(Alg), + is_binary(Key), + is_integer(CounterSecs), + is_integer(StepSecs), + is_integer(OutputLen) -> + couch_hotp:generate(Alg, Key, CounterSecs div StepSecs, OutputLen). diff --git a/test/couch_hotp_tests.erl b/test/couch_hotp_tests.erl new file mode 100644 index 00000000..fee10ff5 --- /dev/null +++ b/test/couch_hotp_tests.erl @@ -0,0 +1,28 @@ +% Licensed 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. + +-module(couch_hotp_tests). + +-include_lib("eunit/include/eunit.hrl"). + +hotp_test() -> + Key = <<"12345678901234567890">>, + ?assertEqual(755224, couch_hotp:generate(sha, Key, 0, 6)), + ?assertEqual(287082, couch_hotp:generate(sha, Key, 1, 6)), + ?assertEqual(359152, couch_hotp:generate(sha, Key, 2, 6)), + ?assertEqual(969429, couch_hotp:generate(sha, Key, 3, 6)), + ?assertEqual(338314, couch_hotp:generate(sha, Key, 4, 6)), + ?assertEqual(254676, couch_hotp:generate(sha, Key, 5, 6)), + ?assertEqual(287922, couch_hotp:generate(sha, Key, 6, 6)), + ?assertEqual(162583, couch_hotp:generate(sha, Key, 7, 6)), + ?assertEqual(399871, couch_hotp:generate(sha, Key, 8, 6)), + ?assertEqual(520489, couch_hotp:generate(sha, Key, 9, 6)). diff --git a/test/couch_totp_tests.erl b/test/couch_totp_tests.erl new file mode 100644 index 00000000..6817a092 --- /dev/null +++ b/test/couch_totp_tests.erl @@ -0,0 +1,55 @@ +% Licensed 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. + +-module(couch_totp_tests). + +-include_lib("eunit/include/eunit.hrl"). + +totp_sha_test() -> + Key = <<"12345678901234567890">>, + ?assertEqual(94287082, couch_totp:generate(sha, Key, 59, 30, 8)), + ?assertEqual(07081804, couch_totp:generate(sha, Key, 1111111109, 30, 8)), + ?assertEqual(14050471, couch_totp:generate(sha, Key, 1111111111, 30, 8)), + ?assertEqual(89005924, couch_totp:generate(sha, Key, 1234567890, 30, 8)), + ?assertEqual(69279037, couch_totp:generate(sha, Key, 2000000000, 30, 8)), + ?assertEqual(65353130, couch_totp:generate(sha, Key, 20000000000, 30, 8)). + +totp_sha256_test() -> + Key = <<"12345678901234567890123456789012">>, + case sha_256_512_supported() of + true -> + ?assertEqual(46119246, couch_totp:generate(sha256, Key, 59, 30, 8)), + ?assertEqual(68084774, couch_totp:generate(sha256, Key, 1111111109, 30, 8)), + ?assertEqual(67062674, couch_totp:generate(sha256, Key, 1111111111, 30, 8)), + ?assertEqual(91819424, couch_totp:generate(sha256, Key, 1234567890, 30, 8)), + ?assertEqual(90698825, couch_totp:generate(sha256, Key, 2000000000, 30, 8)), + ?assertEqual(77737706, couch_totp:generate(sha256, Key, 20000000000, 30, 8)); + false -> + ?debugMsg("sha256 not supported, tests skipped") + end. + +totp_sha512_test() -> + Key = <<"1234567890123456789012345678901234567890123456789012345678901234">>, + case sha_256_512_supported() of + true -> + ?assertEqual(90693936, couch_totp:generate(sha512, Key, 59, 30, 8)), + ?assertEqual(25091201, couch_totp:generate(sha512, Key, 1111111109, 30, 8)), + ?assertEqual(99943326, couch_totp:generate(sha512, Key, 1111111111, 30, 8)), + ?assertEqual(93441116, couch_totp:generate(sha512, Key, 1234567890, 30, 8)), + ?assertEqual(38618901, couch_totp:generate(sha512, Key, 2000000000, 30, 8)), + ?assertEqual(47863826, couch_totp:generate(sha512, Key, 20000000000, 30, 8)); + false -> + ?debugMsg("sha512 not supported, tests skipped") + end. + +sha_256_512_supported() -> + erlang:function_exported(crypto, hmac, 3).