diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9a45ef214..cc61884ac 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -94,7 +94,7 @@ Run the server: ```sh cd specter-desktop -export FLASK_ENV=development && python3 -m cryptoadvance.specter server --config DevelopmentConfig +python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug ``` #### If `pip install` fails on `cryptography==3.4.x` diff --git a/requirements.in b/requirements.in index f6d5182ae..579c40905 100644 --- a/requirements.in +++ b/requirements.in @@ -22,3 +22,4 @@ flask_wtf==0.14.3 pgpy==0.5.4 cbor==1.0.0 mnemonic==0.20 +cryptography==3.4.7 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9659345dd..61ba6eca8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,32 +1,34 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile --generate-hashes requirements.in # aniso8601==9.0.1 \ --hash=sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f \ - --hash=sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973 \ + --hash=sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973 # via flask-restful babel==2.9.1 \ --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ - --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 \ + --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 # via flask-babel base58==2.1.0 \ --hash=sha256:171a547b4a3c61e1ae3807224a6f7aec75e364c4395e7562649d7335768001a2 \ - --hash=sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48 \ + --hash=sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48 # via bitbox02 bitbox02==5.3.0 \ --hash=sha256:797e6904d431f6d2ef711f169e7ce8fffc125cc8c5b3efb8187fd451f45635e1 \ - --hash=sha256:fe0e8aeb9b32fd7d76bb3e9838895973a74dfd532a8fb8ac174a1a60214aee26 \ + --hash=sha256:fe0e8aeb9b32fd7d76bb3e9838895973a74dfd532a8fb8ac174a1a60214aee26 # via hwi cbor==1.0.0 \ - --hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6 \ + --hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6 # via -r requirements.in certifi==2019.9.11 \ --hash=sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50 \ - --hash=sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef \ - # via -r requirements.in, requests + --hash=sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef + # via + # -r requirements.in + # requests cffi==1.14.5 \ --hash=sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813 \ --hash=sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373 \ @@ -76,20 +78,22 @@ cffi==1.14.5 \ --hash=sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406 \ --hash=sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333 \ --hash=sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d \ - --hash=sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c \ + --hash=sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c # via cryptography chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ - --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \ + --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 # via -r requirements.in charset-normalizer==2.0.6 \ --hash=sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6 \ - --hash=sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f \ + --hash=sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f # via requests click==7.1.2 \ --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \ - --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ - # via -r requirements.in, flask + --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc + # via + # -r requirements.in + # flask cryptography==3.4.7 \ --hash=sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d \ --hash=sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959 \ @@ -102,42 +106,55 @@ cryptography==3.4.7 \ --hash=sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250 \ --hash=sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca \ --hash=sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d \ - --hash=sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9 \ - # via noiseprotocol, pgpy, pyopenssl + --hash=sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9 + # via + # -r requirements.in + # noiseprotocol + # pgpy + # pyopenssl ecdsa==0.17.0 \ --hash=sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676 \ - --hash=sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa \ - # via bitbox02, hwi + --hash=sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa + # via + # bitbox02 + # hwi embit==0.4.10 \ - --hash=sha256:f6484bc495b45da27f3eb7fbe21a24c00cd72c0ab83c6e195660cf17db5cb5e2 \ - # via -r requirements.in -flask-babel==2.0.0 \ - --hash=sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468 \ - --hash=sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d \ - # via -r requirements.in -flask-cors==3.0.10 \ - --hash=sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438 \ - --hash=sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de \ - # via -r requirements.in -flask-httpauth==4.4.0 \ - --hash=sha256:bcaaa7a35a3cba0b2eafd4f113b3016bf70eb78087456d96484c3c18928b813a \ - --hash=sha256:d9131122cdc5709dda63790f6e9b3142d8101447d424b0b95ffd4ee279f49539 \ - # via -r requirements.in -flask-login==0.5.0 \ - --hash=sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b \ - --hash=sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0 \ - # via -r requirements.in -flask-restful==0.3.9 \ - --hash=sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2 \ - --hash=sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e \ + --hash=sha256:f6484bc495b45da27f3eb7fbe21a24c00cd72c0ab83c6e195660cf17db5cb5e2 # via -r requirements.in flask==1.1.4 \ --hash=sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196 \ - --hash=sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22 \ - # via -r requirements.in, flask-babel, flask-cors, flask-httpauth, flask-login, flask-restful, flask-wtf + --hash=sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22 + # via + # -r requirements.in + # flask-babel + # flask-cors + # flask-httpauth + # flask-login + # flask-restful + # flask-wtf +flask-babel==2.0.0 \ + --hash=sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468 \ + --hash=sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d + # via -r requirements.in +flask-cors==3.0.10 \ + --hash=sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438 \ + --hash=sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de + # via -r requirements.in +flask-httpauth==4.4.0 \ + --hash=sha256:bcaaa7a35a3cba0b2eafd4f113b3016bf70eb78087456d96484c3c18928b813a \ + --hash=sha256:d9131122cdc5709dda63790f6e9b3142d8101447d424b0b95ffd4ee279f49539 + # via -r requirements.in +flask-login==0.5.0 \ + --hash=sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b \ + --hash=sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0 + # via -r requirements.in +flask-restful==0.3.9 \ + --hash=sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2 \ + --hash=sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e + # via -r requirements.in flask_wtf==0.14.3 \ --hash=sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2 \ - --hash=sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720 \ + --hash=sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720 # via -r requirements.in hidapi==0.10.1 \ --hash=sha256:095798ae1b3d6892fb0eb7ba1ab06054f6fafe6d09bc3714d80fdbf227c98f87 \ @@ -160,28 +177,34 @@ hidapi==0.10.1 \ --hash=sha256:b1becc9f09c85c473e91cf869b592d5d87fb8b89672988de33776b20b4c53ce1 \ --hash=sha256:b686b2b547890c8ed17ebeabded0050ce377180a56daefa20822b4d66d3a5dea \ --hash=sha256:df4a23cd03f00d5cdc603252650df82cdd1923ceef6811cb029cc9d11a9a7a61 \ - --hash=sha256:f49a0de45217366b85597c2edb4be8bd61c9f26f533b854b058dded4352dd89d \ - # via bitbox02, hwi + --hash=sha256:f49a0de45217366b85597c2edb4be8bd61c9f26f533b854b058dded4352dd89d + # via + # bitbox02 + # hwi hwi==2.0.1 \ --hash=sha256:1a49ec86d4770239408e74c87acdd15b9f72271fbdddf66acd935f068bbf115d \ - --hash=sha256:cba5e254fcf7ae1b523ec1f0eab693b401d4b3cd949b35a4557a106a98065c23 \ + --hash=sha256:cba5e254fcf7ae1b523ec1f0eab693b401d4b3cd949b35a4557a106a98065c23 # via -r requirements.in idna==2.10 \ --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ - --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \ + --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 # via requests importlib_metadata==4.8.1 \ --hash=sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15 \ - --hash=sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1 \ + --hash=sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1 # via -r requirements.in itsdangerous==1.1.0 \ --hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \ - --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 \ - # via flask, flask-wtf + --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 + # via + # flask + # flask-wtf jinja2==2.11.3 \ --hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \ - --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 \ - # via flask, flask-babel + --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 + # via + # flask + # flask-babel libusb1==1.9.2 \ --hash=sha256:27aec6aa1ff9ca845d0035023f3cf39710afac56903c51cd96a95404d064189e \ --hash=sha256:2dff68819350bf8a8c157c7fa40d3efc741cb57868687d1714c8125ee99e8ac8 \ @@ -189,7 +212,7 @@ libusb1==1.9.2 \ --hash=sha256:a323588902fbd3693f8fddd7eac016700b24116c31b00756b9f52cf06c2a6629 \ --hash=sha256:b4f25a2d66f62ec740edba3597038a7e9cd45b43456acfdb7a2bca8c2ad4aa30 \ --hash=sha256:c19d49136ef262474dbbac8bd40a2c4b65660220571de8564efec631c56bdc09 \ - --hash=sha256:c3dd4df43b5c38f65bf599413810d021f5f98396c4b6f66765fb98193aca11b0 \ + --hash=sha256:c3dd4df43b5c38f65bf599413810d021f5f98396c4b6f66765fb98193aca11b0 # via hwi markupsafe==2.0.1 \ --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ @@ -225,19 +248,23 @@ markupsafe==2.0.1 \ --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ - --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 \ - # via jinja2, wtforms + --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 + # via + # jinja2 + # wtforms mnemonic==0.20 \ --hash=sha256:7c6fb5639d779388027a77944680aee4870f0fcd09b1e42a5525ee2ce4c625f6 \ - --hash=sha256:acd2168872d0379e7a10873bb3e12bf6c91b35de758135c4fbd1015ef18fafc5 \ - # via -r requirements.in, hwi + --hash=sha256:acd2168872d0379e7a10873bb3e12bf6c91b35de758135c4fbd1015ef18fafc5 + # via + # -r requirements.in + # hwi noiseprotocol==0.3.1 \ --hash=sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111 \ - --hash=sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645 \ + --hash=sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645 # via bitbox02 pgpy==0.5.4 \ --hash=sha256:bdd3da1e006fc8e81cc02232969924d6e8c98a4af1621a925d99bba09164183b \ - --hash=sha256:c29ad9b2bcba6575c3773410894e77a7552b6a3de184fd99b4da3995986f26a9 \ + --hash=sha256:c29ad9b2bcba6575c3773410894e77a7552b6a3de184fd99b4da3995986f26a9 # via -r requirements.in protobuf==3.17.3 \ --hash=sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637 \ @@ -262,7 +289,7 @@ protobuf==3.17.3 \ --hash=sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683 \ --hash=sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47 \ --hash=sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62 \ - --hash=sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b \ + --hash=sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b # via bitbox02 psutil==5.7.3 \ --hash=sha256:01bc82813fbc3ea304914581954979e637bcc7084e59ac904d870d6eb8bb2bc7 \ @@ -275,75 +302,87 @@ psutil==5.7.3 \ --hash=sha256:af73f7bcebdc538eda9cc81d19db1db7bf26f103f91081d780bbacfcb620dee2 \ --hash=sha256:e02c31b2990dcd2431f4524b93491941df39f99619b0d312dfe1d4d530b08b4b \ --hash=sha256:fa38ac15dbf161ab1e941ff4ce39abd64b53fec5ddf60c23290daed2bc7d1157 \ - --hash=sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7 \ + --hash=sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7 # via -r requirements.in pyaes==1.6.1 \ - --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f \ + --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f # via hwi pyasn1==0.4.8 \ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ - --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba \ + --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba # via pgpy pycparser==2.20 \ --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \ - --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 \ + --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 # via cffi pyopenssl==20.0.1 \ --hash=sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51 \ - --hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b \ + --hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b # via -r requirements.in pyserial==3.4 \ --hash=sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627 \ - --hash=sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8 \ + --hash=sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8 # via -r requirements.in pysocks==1.7.1 \ --hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \ --hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \ - --hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0 \ + --hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0 # via -r requirements.in python-dotenv==0.13.0 \ --hash=sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7 \ - --hash=sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74 \ + --hash=sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74 # via -r requirements.in pytz==2021.1 \ --hash=sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da \ - --hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798 \ - # via babel, flask-babel, flask-restful + --hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798 + # via + # babel + # flask-babel + # flask-restful requests==2.26.0 \ --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \ - --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 \ + --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 # via -r requirements.in semver==2.13.0 \ --hash=sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4 \ - --hash=sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f \ + --hash=sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f # via bitbox02 six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ - # via -r requirements.in, ecdsa, flask-cors, flask-restful, pgpy, protobuf, pyopenssl + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via + # -r requirements.in + # ecdsa + # flask-cors + # flask-restful + # pgpy + # protobuf + # pyopenssl stem==1.8.0 \ - --hash=sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2 \ + --hash=sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2 # via -r requirements.in typing-extensions==3.10.0.0 \ --hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \ --hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342 \ - --hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \ - # via bitbox02, hwi + --hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 + # via + # bitbox02 + # hwi urllib3==1.26.5 \ --hash=sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c \ - --hash=sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098 \ + --hash=sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098 # via requests werkzeug==1.0.1 \ --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \ - --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c \ + --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c # via flask wtforms==2.3.3 \ --hash=sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c \ - --hash=sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c \ + --hash=sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c # via flask-wtf zipp==3.4.1 \ --hash=sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76 \ - --hash=sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 \ + --hash=sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 # via importlib-metadata # WARNING: The following packages were not pinned, but pip requires them to be diff --git a/src/cryptoadvance/specter/api/security.py b/src/cryptoadvance/specter/api/security.py index eef34675a..d7626fca3 100644 --- a/src/cryptoadvance/specter/api/security.py +++ b/src/cryptoadvance/specter/api/security.py @@ -25,14 +25,14 @@ def verify_password(username, password): if not the_user: return abort(401) g.user = app.specter.user_manager.get_user_by_username(username) - if user_verify_password(g.user.password, password): + if user_verify_password(g.user.password_hash, password): logger.info(f"Rest-Request for user {username} PASSED password-test") return username else: logger.info(f"Rest-Request for user {username} FAILED password-test") return abort(401) - return g.user is not None and verify_password(g.user.password, password) + return g.user is not None and verify_password(g.user.password_hash, password) def require_admin(func): diff --git a/src/cryptoadvance/specter/managers/config_manager.py b/src/cryptoadvance/specter/managers/config_manager.py index 93998d46c..fbfe7f3d3 100644 --- a/src/cryptoadvance/specter/managers/config_manager.py +++ b/src/cryptoadvance/specter/managers/config_manager.py @@ -4,8 +4,8 @@ import random import secrets import threading import time -from urllib.parse import urlparse +from urllib.parse import urlparse from ..helpers import deep_update from ..persistence import read_json_file, write_json_file @@ -19,7 +19,7 @@ class ConfigManager(GenericDataManager): """ The ConfigManager manages the configuration persisted in config.json It's not suppose to have any side-effects. Setting and getting only - with a lot of validation and computing while setting/getting + with a lot of validation and computing while setting/getting. """ name_of_json_file = "config.json" @@ -70,6 +70,8 @@ class ConfigManager(GenericDataManager): # TODO: remove "bitcoind": False, } + + # TODO: change this to just override load() self.check_config() def check_config(self): diff --git a/src/cryptoadvance/specter/managers/genericdata_manager.py b/src/cryptoadvance/specter/managers/genericdata_manager.py index 0e804ae4a..eede0cdb0 100644 --- a/src/cryptoadvance/specter/managers/genericdata_manager.py +++ b/src/cryptoadvance/specter/managers/genericdata_manager.py @@ -1,25 +1,40 @@ -import os +import copy import logging +import os + +from cryptography.fernet import Fernet + from ..persistence import read_json_file, write_json_file + logger = logging.getLogger(__name__) class GenericDataManager: """ A GenericDataManager manages json-data in self.data in a json file. It's meant to - be derived from. See OtpManager + be derived from. See OtpManager. + + Supports encrypting individual attributes rest. Expects a Fernet key that is unique + to the user to encrypt/decrypt. """ + name_of_json_file = "some_data.json" + encrypted_fields = [] + + # Track any changes to our encryption implementation so we can migrate up any older + # data as we go. + encrypted_storage_version = 1 + @classmethod def initial_data(cls): return {} - name_of_json_file = "some_data.json" - - # of them via json-files in an empty data folder - def __init__(self, data_folder): + def __init__(self, data_folder, encryption_key=None): + # encryption_key indicates that the encrypted_fields (if any) need to be + # encrypted at rest. self.data_folder = data_folder + self.encryption_key = encryption_key self.load() @property @@ -33,30 +48,41 @@ class GenericDataManager: self.data = read_json_file(self.data_file) # otherwise - create one and assign unique id else: - logger.debug(f"{self.data_file} not existing. Creating ...") + logger.debug(f"{self.data_file} doesn't exist. Creating ...") self.data = self.__class__.initial_data() self._save() + if self.encryption_key: + # Decrypt the encrypted fields and store in memory as plaintext + fernet = Fernet(self.encryption_key) + + if ( + "encrypted_storage_version" in self.data + and self.encrypted_storage_version + != self.data["encrypted_storage_version"] + ): + raise Exception( + "Upgrading a previous encryption version is not yet implemented" + ) + + for attr in self.encrypted_fields: + if attr in self.data and self.data[attr] is not None: + self.data[attr] = fernet.decrypt(self.data[attr].encode()).decode() + def _save(self): - # data_json = convert_to_list_of_dict(self.data) - write_json_file(self.data, self.data_file) + if self.encryption_key: + # Preserve the in-memory data but encrypt a copy to write to disk + fernet = Fernet(self.encryption_key) + output_dict = copy.deepcopy(self.data) + for attr in self.encrypted_fields: + if attr in output_dict and output_dict[attr] is not None: + output_dict[attr] = fernet.encrypt( + output_dict[attr].encode() + ).decode() + else: + output_dict = self.data - @classmethod - def convert_to_list_of_type(cls, some_list): - """This is not yet used but might be convenient - An array of elements get converted to a list of a specific types - Maybe this method simply does nothing, though - """ - return some_list - # example implementation for users: - # return [User.from_json(u, self.specter) for u in some_list] + if self.encrypted_fields: + output_dict["encrypted_storage_version"] = self.encrypted_storage_version - @classmethod - def convert_to_list_of_dict(cls, some_list): - """This is not yet used but might be convenient - An array of a specific type gets converted to a list of dicts - Maybe this method simply does nothing, though - """ - return some_list - # example implementation for users: - # return [u.json for u in some_list] + write_json_file(output_dict, self.data_file) diff --git a/src/cryptoadvance/specter/managers/user_manager.py b/src/cryptoadvance/specter/managers/user_manager.py index 26f3078f9..4912d783c 100644 --- a/src/cryptoadvance/specter/managers/user_manager.py +++ b/src/cryptoadvance/specter/managers/user_manager.py @@ -35,6 +35,7 @@ class UserManager: "username": "admin", "password": hash_password("admin"), "is_admin": True, + "encrypted_user_secret": None, } ] # convert to User instances @@ -58,6 +59,12 @@ class UserManager: user.check() return self.get_user(user) + def create_user(self, user_id, username, plaintext_password, config): + password_hash = hash_password(plaintext_password) + user = User(user_id, username, password_hash, config, self.specter) + user.decrypt_user_secret(plaintext_password) + return self.add_user(user) + @property def admin(self): """There is always one admin""" diff --git a/src/cryptoadvance/specter/persistence.py b/src/cryptoadvance/specter/persistence.py index f4e9dfa7f..324a5d6e1 100644 --- a/src/cryptoadvance/specter/persistence.py +++ b/src/cryptoadvance/specter/persistence.py @@ -29,6 +29,7 @@ def read_json_file(path): try: with open(path, "r") as f: content = json.load(f) + # if failed - try reading from the backup except Exception as e: # if no backup exists - raise @@ -70,9 +71,11 @@ def _write_json_file(content, path, lock=None): try: with open(path, "w") as f: json.dump(content, f, indent=4) - # check if write was sucessfull + + # check if write was sucessful with open(path, "r") as f: c = json.load(f) + # if not - move back backup except Exception as e: # remove damaged file diff --git a/src/cryptoadvance/specter/server.py b/src/cryptoadvance/specter/server.py index ef41862ce..ba9bff783 100644 --- a/src/cryptoadvance/specter/server.py +++ b/src/cryptoadvance/specter/server.py @@ -133,8 +133,14 @@ def init_app(app, hwibridge=False, specter=None): def user_loader(id): return specter.user_manager.get_user(id) - def login(id): - login_user(user_loader(id)) + def login(id, password: str = None): + user = user_loader(id) + login_user(user) + + if password: + # Use the password while we have it to decrypt any protected + # user data (e.g. services). + user.decrypt_user_secret(password) app.login = login # Attach specter instance so child views (e.g. hwi) can access it diff --git a/src/cryptoadvance/specter/server_endpoints/auth.py b/src/cryptoadvance/specter/server_endpoints/auth.py index 34be59dab..5032f4826 100644 --- a/src/cryptoadvance/specter/server_endpoints/auth.py +++ b/src/cryptoadvance/specter/server_endpoints/auth.py @@ -35,6 +35,7 @@ def login(): app.login("admin") app.logger.info("AUDIT: Successful Login no credentials") return redirect_login(request) + if auth["method"] == "rpcpasswordaspin": # TODO: check the password via RPC-call if ( @@ -42,11 +43,12 @@ def login(): or not app.specter.default_node.rpc.test_connection() ): if app.specter.default_node.password == request.form["password"]: - app.login("admin") + app.login("admin", request.form["password"]) app.logger.info( "AUDIT: Successfull Login via RPC-credentials (node disconnected)" ) return redirect_login(request) + flash( _( "We could not check your password, maybe Bitcoin Core is not running or not configured?" @@ -65,23 +67,26 @@ def login(): rpc = app.specter.default_node.rpc.clone() rpc.password = request.form["password"] if rpc.test_connection(): - app.login("admin") + app.login("admin", request.form["password"]) app.logger.info("AUDIT: Successfull Login via RPC-credentials") return redirect_login(request) + elif auth["method"] == "passwordonly": password = request.form["password"] - if verify_password(app.specter.user_manager.admin.password, password): - app.login("admin") + if verify_password(app.specter.user_manager.admin.password_hash, password): + app.login("admin", request.form["password"]) return redirect_login(request) + elif auth["method"] == "usernamepassword": # TODO: This way both "User" and "user" will pass as usernames, should there be strict check on that here? Or should we keep it like this? username = request.form["username"] password = request.form["password"] user = app.specter.user_manager.get_user_by_username(username) if user: - if verify_password(user.password, password): - app.login(user.id) + if verify_password(user.password_hash, password): + app.login(user.id, request.form["password"]) return redirect_login(request) + # Either invalid method or incorrect credentials flash(_("Invalid username or password"), "error") app.logger.info("AUDIT: Invalid password login attempt") @@ -141,9 +146,14 @@ def register(): "explorers": {"main": "", "test": "", "regtest": "", "signet": ""}, "hwi_bridge_url": "/hwi/api/", } - password_hash = hash_password(password) - user = User(user_id, username, password_hash, config, app.specter) - app.specter.user_manager.add_user(user) + + user = app.specter.user_manager.create_user( + user_id=user_id, + username=username, + plaintext_password=password, + config=config, + ) + flash( _( "You have registered successfully, \ diff --git a/src/cryptoadvance/specter/server_endpoints/settings.py b/src/cryptoadvance/specter/server_endpoints/settings.py index d45b7e45c..e41518a47 100644 --- a/src/cryptoadvance/specter/server_endpoints/settings.py +++ b/src/cryptoadvance/specter/server_endpoints/settings.py @@ -27,7 +27,6 @@ from ..helpers import ( ) from ..persistence import write_devices, write_wallet from ..specter_error import ExtProcTimeoutException, handle_exception -from ..user import hash_password from ..util.sha256sum import sha256sum from ..util.shell import get_last_lines_from_file from ..util.tor import start_hidden_service, stop_hidden_services @@ -398,10 +397,12 @@ def auth(): else: specter_username = None specter_password = None + if current_user.is_admin: method = request.form["method"] rate_limit = request.form["rate_limit"] registration_link_timeout = request.form["registration_link_timeout"] + min_chars = int(auth["password_min_chars"]) if specter_username: if current_user.username != specter_username: @@ -439,34 +440,37 @@ def auth(): current_version=current_version, rand=rand, ) - current_user.password = hash_password(specter_password) + current_user.set_password(specter_password) current_user.save_info() if current_user.is_admin: app.specter.update_auth(method, rate_limit, registration_link_timeout) if method in ["rpcpasswordaspin", "passwordonly", "usernamepassword"]: if method == "passwordonly": - new_password = request.form.get("specter_password_only", "") - if new_password: - if len(new_password) < min_chars: - flash( - _( - "Please enter a password of a least {} characters" - ).format(min_chars), - "error", - ) - return render_template( - "settings/auth_settings.jinja", - method=method, - rate_limit=rate_limit, - registration_link_timeout=registration_link_timeout, - users=users, - specter=app.specter, - current_version=current_version, - rand=rand, - ) + new_password = request.form.get("specter_password_only") + if new_password and len(new_password) < min_chars: + flash( + _( + "Please enter a password of a least {} characters" + ).format(min_chars), + "error", + ) + return render_template( + "settings/auth_settings.jinja", + method=method, + rate_limit=rate_limit, + registration_link_timeout=registration_link_timeout, + users=users, + specter=app.specter, + current_version=current_version, + rand=rand, + ) + elif not new_password: + # Set to the default + new_password = "admin" + + current_user.set_password(new_password) + current_user.save_info() - current_user.password = hash_password(new_password) - current_user.save_info() if method == "usernamepassword": users = [ user @@ -481,6 +485,7 @@ def auth(): app.config["LOGIN_DISABLED"] = True app.specter.check() + elif action == "adduser": if current_user.is_admin: new_otp = secrets.token_urlsafe(16) @@ -510,10 +515,12 @@ def auth(): _("Error: Only the admin account can issue new registration links"), "error", ) + elif action == "deleteuser": delete_user = request.form["deleteuser"] user = app.specter.user_manager.get_user(delete_user) if current_user.is_admin: + # TODO: delete should be done by UserManager app.specter.delete_user(user) users.remove(user) flash( @@ -521,6 +528,7 @@ def auth(): ) else: flash(_("Error: Only the admin account can delete users"), "error") + return render_template( "settings/auth_settings.jinja", method=method, diff --git a/src/cryptoadvance/specter/user.py b/src/cryptoadvance/specter/user.py index 1b32e63f7..536c0e623 100644 --- a/src/cryptoadvance/specter/user.py +++ b/src/cryptoadvance/specter/user.py @@ -1,22 +1,31 @@ +import base64 +import binascii +import hashlib +import json import os import shutil -import hashlib -import binascii -import json + +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + from flask_login import UserMixin -from .specter_error import SpecterError + +from .specter_error import SpecterError, handle_exception from .persistence import read_json_file, write_json_file, delete_folder from .managers.wallet_manager import WalletManager from .managers.device_manager import DeviceManager from .helpers import deep_update -def hash_password(password): +def hash_password(plaintext_password): """Hash a password for storing.""" salt = binascii.b2a_base64(hashlib.sha256(os.urandom(60)).digest()).strip() pwdhash = ( binascii.b2a_base64( - hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 10000) + hashlib.pbkdf2_hmac( + "sha256", plaintext_password.encode("utf-8"), salt, 10000 + ) ) .strip() .decode() @@ -36,17 +45,68 @@ def verify_password(stored_password, provided_password): class User(UserMixin): - def __init__(self, id, username, password, config, specter, is_admin=False): + """ + The user_secret is used to encrypt/decrypt other user-specific data + (e.g. services data). It is encrypted for storage using the user's + password (the method == "none" auth type cannot enable options that + require encrypted storage because it has no password). user_secret is + decrypted on login (see: SpecterFlask.login()) and stored in memory + in plaintext. + """ + + def __init__( + self, + id, + username, + password_hash, + config, + specter, + encrypted_user_secret=None, + is_admin=False, + services=None, + ): self.id = id self.username = username - self.password = password + self.password_hash = password_hash self.config = config + self.encrypted_user_secret = encrypted_user_secret + self.plaintext_user_secret = None self.is_admin = is_admin self.uid = specter.config["uid"] self.specter = specter self.wallet_manager = None self.device_manager = None self.manager = None + self.services = services + + # Iterations will need to be increased over time to keep ahead of CPU advances. + self.encryption_iterations = 390000 + + # TODO: User obj instantiation belongs in UserManager + @classmethod + def from_json(cls, user_dict, specter): + try: + user_args = { + "id": user_dict["id"], + "username": user_dict["username"], + "password_hash": user_dict[ + "password" + ], # TODO: Migrate attr name to "password_hash"? + "config": {}, + "specter": specter, + "encrypted_user_secret": user_dict.get("encrypted_user_secret", None), + "services": user_dict.get("services", None), + } + if not user_dict["is_admin"]: + user_args["config"] = user_dict["config"] + return cls(**user_args) + else: + user_args["is_admin"] = True + return cls(**user_args) + + except Exception as e: + handle_exception(e) + raise SpecterError(f"Unable to parse user JSON.:{e}") @property def folder_id(self): @@ -54,51 +114,85 @@ class User(UserMixin): return "" return f"_{self.id}" - @property - def password(self): - return self._password + def _encrypt_user_secret(self, plaintext_password): + """ + Implementation taken from the pyca/cryptography docs: + https://cryptography.io/en/latest/fernet/#using-passwords-with-fernet + """ + salt = os.urandom(16) - @password.setter - def password(self, value): - """pass a json or a plain-password here""" - try: - if value.get("salt") and value.get("pwdhash"): - self._password = value - except: - salted_hashed_password = hash_password(value) - self._password = salted_hashed_password + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=self.encryption_iterations, + ) + key = base64.urlsafe_b64encode(kdf.derive(plaintext_password.encode())) + f = Fernet(key) + token = f.encrypt(self.plaintext_user_secret) - @classmethod - def from_json(cls, user_dict, specter): - # TODO: Unify admin in backwards compatible way - try: - if not user_dict["is_admin"]: - return cls( - user_dict["id"], - user_dict["username"], - user_dict["password"], - user_dict["config"], - specter, + self.encrypted_user_secret = { + "token": token.decode(), + "salt": base64.b64encode(salt).decode(), + "iterations": self.encryption_iterations, + } + + def decrypt_user_secret(self, plaintext_password): + # see: https://cryptography.io/en/latest/fernet/#using-passwords-with-fernet + if not self.encrypted_user_secret: + self._generate_user_secret(plaintext_password) + + token = self.encrypted_user_secret["token"].encode() + salt = base64.b64decode(self.encrypted_user_secret["salt"]) + iterations = self.encrypted_user_secret["iterations"] + + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=iterations, + ) + key = base64.urlsafe_b64encode(kdf.derive(plaintext_password.encode())) + f = Fernet(key) + + self.plaintext_user_secret = f.decrypt(token) + + # If this encrypted_user_secret has an outdated (weaker) number of iterations, + # re-encrypt with the current higher iteration count. + if iterations < self.encryption_iterations: + self._encrypt_user_secret(plaintext_password) + + def _generate_user_secret(self, plaintext_password): + # Encryption using the user_secret uses a Fernet key. But the Fernet + # key itself will be encrypted with the user's password. + self.plaintext_user_secret = Fernet.generate_key() + self._encrypt_user_secret(plaintext_password) + self.save_info() + + def set_password(self, plaintext_password): + # Hash the incoming plaintext password and update the encrypted + # user_secret as needed. + self.password_hash = hash_password(plaintext_password) + + # Must keep encrypted_user_secret in sync with password changes + if self.encrypted_user_secret is None: + self._generate_user_secret(plaintext_password) + else: + if self.plaintext_user_secret is None: + raise Exception( + "encrypted_user_secret wasn't decrypted during user login" ) - else: - return cls( - user_dict["id"], - user_dict["username"], - user_dict["password"], - {}, - specter, - is_admin=True, - ) - except Exception as e: - raise SpecterError(f"Unable to parse user JSON.:{e}") + self._encrypt_user_secret(plaintext_password) @property def json(self): user_dict = { "id": self.id, "username": self.username, - "password": self.password, + "password": self.password_hash, # TODO: Migrate attr name to "password_hash"? "is_admin": self.is_admin, + "encrypted_user_secret": self.encrypted_user_secret, + "services": self.services, } if not self.is_admin: user_dict["config"] = self.config @@ -147,6 +241,7 @@ class User(UserMixin): else: self.device_manager.update(data_folder=devices_folder) + # TODO: Refactor this into UserManager def save_info(self, delete=False): if self.manager is None: self.manager = self.specter.user_manager @@ -155,11 +250,14 @@ class User(UserMixin): # update specter users if not existing and not delete: - self.specter.add_user(self) + self.specter.user_manager.add_user(self) if existing and delete: self.specter.delete_user(self) self.manager.save() + # TODO: Refactor calling code to explicitly call User.save() rather than embedding + # self.save_info() on every update and setter. It ends up saving to disk multiple + # times for a single Settings submit. def update_asset_label(self, asset, label, chain): if "asset_labels" not in self.config: self.config["asset_labels"] = {} diff --git a/tests/conftest.py b/tests/conftest.py index 4f29cc534..fae9a9d94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from cryptoadvance.specter.rpc import BitcoinRPC from cryptoadvance.specter.server import create_app, init_app from cryptoadvance.specter.specter import Specter from cryptoadvance.specter.specter_error import SpecterError -from cryptoadvance.specter.user import User +from cryptoadvance.specter.user import User, hash_password from cryptoadvance.specter.util.wallet_importer import WalletImporter from cryptoadvance.specter.util.common import str2bool from cryptoadvance.specter.util.shell import which @@ -388,16 +388,17 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): specter = Specter(data_folder=devices_filled_data_folder, config=config) assert specter.chain == "regtest" # Create a User - someuser: User = specter.user_manager.add_user( + someuser = specter.user_manager.add_user( User.from_json( - { + user_dict={ "id": "someuser", "username": "someuser", - "password": "somepassword", + "password": hash_password("somepassword"), "config": {}, "is_admin": False, + "services": None, }, - specter, + specter=specter, ) ) specter.user_manager.save() diff --git a/tests/test_genericdata_storage.py b/tests/test_genericdata_storage.py new file mode 100644 index 000000000..dd5c60835 --- /dev/null +++ b/tests/test_genericdata_storage.py @@ -0,0 +1,68 @@ +import json, logging, pytest +from cryptoadvance.specter.managers.genericdata_manager import GenericDataManager +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.user import User, hash_password + + +class ExampleStorage(GenericDataManager): + """ + Pretend storage class just for testing + """ + + encrypted_fields = [ + "testfield2", + ] + + +def test_storage_field_encrypt_decrypt(empty_data_folder): + """ + Storage class should be able to use the associated User's decrypted user_secret + to encrypt and decrypt the specified encrypted_fields to and from on-disk + json. When loaded into memory, all fields -- whether encrypted or not -- should + be plaintext readable. + """ + specter = Specter(data_folder=empty_data_folder) + + password = "somepassword" + user = User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password("somepassword"), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + + # User must provide their password in order to decrypt their user_secret which is + # then used to decrypt/encrypt their service storage + user.decrypt_user_secret(password) + + storage = ExampleStorage( + data_folder=specter.data_folder, encryption_key=user.plaintext_user_secret + ) + storage.data["testfield1"] = "This data is not encrypted" + storage.data["testfield2"] = "This data WILL BE encrypted" + storage._save() + + # Read the resulting storage file + with open(storage.data_file, "r") as storage_json_file: + data_on_disk = json.load(storage_json_file) + print(data_on_disk) + + # Plaintext fields are readable... + assert data_on_disk["testfield1"] == storage.data["testfield1"] + + # ...while encrypted fields are not + assert data_on_disk["testfield2"] != storage.data["testfield2"] + + # Re-instantiate the storage so it has to load from the saved file... + storage_2 = ExampleStorage( + data_folder=specter.data_folder, encryption_key=user.plaintext_user_secret + ) + + # ...and verify the field decryption + assert storage_2.data["testfield2"] == storage.data["testfield2"] + assert data_on_disk["testfield2"] != storage_2.data["testfield2"] diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 000000000..3c1d3c72b --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,165 @@ +import json, logging, pytest +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.user import User, hash_password, verify_password +from cryptoadvance.specter.managers.user_manager import UserManager + + +def test_password_hash(): + """ + verify_password should succeed when presented with hash_password and the same + password that was hashed. It should fail when given a different password. + """ + password = "somepassword" + password_hash = hash_password(password) + assert verify_password(password_hash, password) + assert not verify_password(password_hash, "wrongpassword") + + +def test_generate_user_secret_on_decrypt_user_secret(empty_data_folder): + """ + Should generate a user_secret if one does not yet exist when decrypt_user_secret + is called (happens during the login flow). + """ + specter = Specter(data_folder=empty_data_folder) + + password = "somepassword" + user = User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password(password), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + + assert user.encrypted_user_secret is None + assert user.plaintext_user_secret is None + + # Even though there's no user_secret yet, the flow calls decrypt anyway... + user.decrypt_user_secret(password) + + # ...and a new user_secret is created and stored encrypted and plaintext + assert user.encrypted_user_secret is not None + assert user.plaintext_user_secret is not None + + +def test_generate_user_secret_on_set_password(empty_data_folder): + """ + Should generate a user_secret if one does not yet exist when the User's password + is changed via set_password. + """ + specter = Specter(data_folder=empty_data_folder) + + password = "somepassword" + user = User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password(password), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + + assert user.encrypted_user_secret is None + assert user.plaintext_user_secret is None + + new_password = "mynewpassphrase" + user.set_password(new_password) + + assert user.encrypted_user_secret is not None + assert user.plaintext_user_secret is not None + + # Reset the plaintext user_secret and test decryption + user.plaintext_user_secret = None + user.decrypt_user_secret(new_password) + + assert user.plaintext_user_secret is not None + + +def test_reencrypt_user_secret_on_set_password(empty_data_folder): + """ + Should re-encrypt the user_secret when the user changes their password. + """ + specter = Specter(data_folder=empty_data_folder) + + password = "somepassword" + user = User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password(password), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + + # Force generation of a new user_secret + user.decrypt_user_secret(password) + assert user.encrypted_user_secret is not None + assert user.plaintext_user_secret is not None + + first_encrypted_user_secret = user.encrypted_user_secret + first_plaintext_user_secret = user.plaintext_user_secret + + new_password = "mynewpassphrase" + user.set_password(new_password) + + # The new encrypted_user_secret will be different... + assert first_encrypted_user_secret != user.encrypted_user_secret + + # ...but the plaintext_user_secret remains unchanged + assert first_plaintext_user_secret == user.plaintext_user_secret + + +def test_reencrypt_user_secret_on_iterations_increase(empty_data_folder): + """ + Should re-encrypt the user_secret when the User.encryption_iterations is increased + """ + specter = Specter(data_folder=empty_data_folder) + + password = "somepassword" + user = User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password(password), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + + # Override current default iterations setting + original_encryption_iterations = user.encryption_iterations + user.encryption_iterations -= 10000 + + # Force generation of a new user_secret + user.decrypt_user_secret(password) + assert user.encrypted_user_secret is not None + assert user.plaintext_user_secret is not None + assert user.encrypted_user_secret["iterations"] < original_encryption_iterations + + first_encrypted_user_secret = user.encrypted_user_secret + first_plaintext_user_secret = user.plaintext_user_secret + + # Reset iterations to default + user.encryption_iterations = original_encryption_iterations + + # On decrypt, should automatically re-encrypt the `user_secret` + user.decrypt_user_secret(password) + assert user.encrypted_user_secret["iterations"] == original_encryption_iterations + + # The new encrypted_user_secret will be different... + assert first_encrypted_user_secret != user.encrypted_user_secret + + # ...but the plaintext_user_secret remains unchanged + assert first_plaintext_user_secret == user.plaintext_user_secret diff --git a/tests/test_user_manager.py b/tests/test_user_manager.py new file mode 100644 index 000000000..5ba90acba --- /dev/null +++ b/tests/test_user_manager.py @@ -0,0 +1,33 @@ +import json +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.managers.user_manager import UserManager + + +def test_create_user(empty_data_folder): + """ + Should add a new User to the `users` list, generate a user_secret, and write + the new User to json storage. + """ + specter = Specter(data_folder=empty_data_folder) + user_manager = UserManager(specter=specter) + + password = "somepassword" + user_id = "someuser" + username = "someuser" + config = {} + + user = user_manager.create_user( + user_id=user_id, username=username, plaintext_password=password, config=config + ) + + # new User was added to `users` + assert user in user_manager.users + + # Generated a `user_secret` + assert user.encrypted_user_secret is not None + assert user.plaintext_user_secret is not None + + # Already written to persistent storage + with open(user_manager.users_file) as user_json_file: + user_json = json.load(user_json_file) + assert user_id in [u.get("id") for u in user_json]