Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb22101b02 | ||
|
|
44f69e755c | ||
|
|
327b7ccbae | ||
|
|
6585513120 | ||
|
|
acbe73834d | ||
|
|
c27c4f0ac5 | ||
|
|
34cd16f240 | ||
|
|
78fc37d1e9 | ||
|
|
df9684a31c | ||
|
|
520c587d09 | ||
|
|
7506c70236 | ||
|
|
d1e3224cba | ||
|
|
d2eed8a356 | ||
|
|
668823507d | ||
|
|
2b83dde58a | ||
|
|
e97a53bbe9 | ||
|
|
77f59d41ad | ||
|
|
e2ddc8a80f | ||
|
|
b6093c2bfa | ||
|
|
fc25312d12 | ||
|
|
51eade900c | ||
|
|
d152d13580 | ||
|
|
7dff3fbad1 | ||
|
|
7dc7f6ccc0 | ||
|
|
ad0088eca6 | ||
|
|
813bf292fa | ||
|
|
80bf5bc854 | ||
|
|
e319a2eceb | ||
|
|
0a9bfde438 | ||
|
|
c3c4e1fb4b | ||
|
|
1eff91594f | ||
|
|
fddc29880a | ||
|
|
2ccfcd62e1 | ||
|
|
993b29bd83 | ||
|
|
e371485a1b | ||
|
|
fb58c877d1 | ||
|
|
a62e474e33 | ||
|
|
5975b929b7 | ||
|
|
a5d3de55a2 | ||
|
|
b195d0dadd | ||
|
|
7d3666a5d0 | ||
|
|
61ac83b134 | ||
|
|
9c907cf14a | ||
|
|
5f737582fb | ||
|
|
89256dfef5 | ||
|
|
94b4c098de | ||
|
|
1d848947d9 | ||
|
|
43ebe402ce | ||
|
|
ae0b00573f | ||
|
|
efb5b10a76 | ||
|
|
4f38988c0d | ||
|
|
c977b9a064 | ||
|
|
5353e7c3b1 | ||
|
|
b999ccc83b | ||
|
|
fea5b8e75a | ||
|
|
6ce661180e | ||
|
|
230f21b9f0 | ||
|
|
801e677928 | ||
|
|
143b9a85b4 | ||
|
|
71f66e3142 | ||
|
|
a6ab669811 | ||
|
|
a519b31bd3 | ||
|
|
c7ef10367f | ||
|
|
085e8e51dc | ||
|
|
167f2d6dad | ||
|
|
e653e42455 | ||
|
|
158eb4609c | ||
|
|
2242dbc6cd | ||
|
|
ca8f3e49ea | ||
|
|
c3d9ab86c7 | ||
|
|
441a943a9d | ||
|
|
293e2ddeb0 | ||
|
|
2c8a64f943 | ||
|
|
bc6cb35dc0 | ||
|
|
ad138e4a4c | ||
|
|
f4bdf4645b | ||
|
|
fd9a387b4e | ||
|
|
778000aa24 | ||
|
|
40fbe39474 | ||
|
|
bda1a1d5af | ||
|
|
e6e2809c9c | ||
|
|
b91355da47 | ||
|
|
4bb5ee52d9 | ||
|
|
de3fcfdb26 | ||
|
|
ae71febf10 | ||
|
|
7be1a74cfd | ||
|
|
22c93bf5c6 | ||
|
|
68436fba9c | ||
|
|
44ff321f8f | ||
|
|
d9d6733878 | ||
|
|
eae31ef94b | ||
|
|
29ea2df696 | ||
|
|
a6bf3b7c87 | ||
|
|
c6cb390990 | ||
|
|
f7a428b8cb | ||
|
|
f080eaa8f9 | ||
|
|
07c768893a | ||
|
|
ef8c116bdc | ||
|
|
260a3b9c78 | ||
|
|
a5ec7666ec | ||
|
|
ff450c3edf | ||
|
|
e6e179307d | ||
|
|
2c0068bea7 | ||
|
|
6dcde38667 | ||
|
|
ec1913c77a | ||
|
|
3e40cc9c8f | ||
|
|
40d1e22079 | ||
|
|
38bbb60414 | ||
|
|
98d39ee399 | ||
|
|
e324f649ee | ||
|
|
4f8a0b9020 | ||
|
|
b78b22acbb | ||
|
|
737b9da292 | ||
|
|
dea13f56f5 | ||
|
|
1f0c51b45b | ||
|
|
466e296988 | ||
|
|
fb17f48013 | ||
|
|
f203eab3ef | ||
|
|
1bd9cf3a3f | ||
|
|
a795015487 | ||
|
|
0a6c89cd9d | ||
|
|
951241e95d | ||
|
|
ddbe9ccfb7 | ||
|
|
1d44e05866 | ||
|
|
4482a21a54 | ||
|
|
15e28abecd | ||
|
|
1748e2221f | ||
|
|
b1b4c021cf | ||
|
|
a27e3c6364 | ||
|
|
8f7b152af4 | ||
|
|
39f18b79cb | ||
|
|
9a81cccbe2 | ||
|
|
2c6be39f88 | ||
|
|
a6299ffb5f | ||
|
|
6e5a80a000 | ||
|
|
f012c7ce22 | ||
|
|
805a9001e1 | ||
|
|
884a1d07c4 | ||
|
|
e9f6371885 | ||
|
|
1f2e45fd12 | ||
|
|
6a5323b12b | ||
|
|
735c086726 | ||
|
|
d94be2420c | ||
|
|
0b6da5a93c | ||
|
|
d24e1471a7 | ||
|
|
7ca072a29c | ||
|
|
d5ac447332 | ||
|
|
0067c79dfa | ||
|
|
4e198d2556 | ||
|
|
f15a0018ef | ||
|
|
d121139676 | ||
|
|
98b246ad20 | ||
|
|
bce0d2c044 | ||
|
|
bf91917745 | ||
|
|
a91339de73 | ||
|
|
5d9de7ac32 | ||
|
|
5f2529ea33 | ||
|
|
61364b5542 | ||
|
|
69196ddbcc | ||
|
|
b21b6dc750 | ||
|
|
e14a61dbf6 | ||
|
|
e8f94968c9 | ||
|
|
8b7e5d265b | ||
|
|
22a70ac626 | ||
|
|
dd83b74251 | ||
|
|
5a9da45a8e | ||
|
|
cac13b082e | ||
|
|
86c3c401ae | ||
|
|
33ff7da7af | ||
|
|
0af48f8bfa | ||
|
|
01885e1fc6 | ||
|
|
52d2713482 | ||
|
|
d875cb6baa | ||
|
|
0cdb882991 | ||
|
|
31d382d6ad | ||
|
|
141f491ca7 | ||
|
|
c0bbe50996 | ||
|
|
875c752f78 | ||
|
|
1d00a2d8d6 | ||
|
|
14407412bf | ||
|
|
bb2716f640 | ||
|
|
0f6b0dddbe | ||
|
|
d43e5e04c7 | ||
|
|
4c86e154c1 | ||
|
|
a45c834b9c | ||
|
|
b746ca0c6e | ||
|
|
7d38bc9b71 | ||
|
|
9557b60c93 | ||
|
|
227b5fd87c | ||
|
|
7d063104dd | ||
|
|
c03d00695d | ||
|
|
171c774b21 | ||
|
|
97944aac88 | ||
|
|
ca21e0bdd1 | ||
|
|
83c1914ddc | ||
|
|
d660fbc6d1 | ||
|
|
35b5ce9d17 | ||
|
|
762fb9bccd | ||
|
|
c3d38c18a3 | ||
|
|
7a3ad11902 | ||
|
|
28fcd68b72 | ||
|
|
c2ed4b4849 | ||
|
|
9bbc2886c2 | ||
|
|
5886462d2c | ||
|
|
98e8a0d97e | ||
|
|
0368a992de | ||
|
|
233eeb6aa1 | ||
|
|
e5d162be9b | ||
|
|
718d575a59 | ||
|
|
59e8c23a01 | ||
|
|
847009d733 | ||
|
|
ce3d674da8 | ||
|
|
9e4313e138 | ||
|
|
02e01723b2 | ||
|
|
874f5e9058 | ||
|
|
7518408554 | ||
|
|
6d68625ee2 | ||
|
|
0e78fd9652 | ||
|
|
9d44d6abb2 | ||
|
|
42c308bc8f | ||
|
|
20fb531f91 | ||
|
|
b8b3da4f85 | ||
|
|
7e403c2af6 | ||
|
|
be0529a0c8 | ||
|
|
9c5f136f5b | ||
|
|
8c34ce8f38 | ||
|
|
3cdf31f940 | ||
|
|
7059f3be74 | ||
|
|
0c0e8c0d66 | ||
|
|
06fd77b5f0 | ||
|
|
9121922ef2 | ||
|
|
2347a322b5 | ||
|
|
fcd3623eef | ||
|
|
121ba06f5a | ||
|
|
6611aa851c | ||
|
|
c0b8b2c7ee | ||
|
|
34888767ed | ||
|
|
61f2e6e020 | ||
|
|
ba3e6203ec | ||
|
|
906d1b453a | ||
|
|
a9ccf412e7 | ||
|
|
21b588dc8b | ||
|
|
90e5003418 | ||
|
|
f8cf87aae9 | ||
|
|
a77b308896 | ||
|
|
a25dbc5cdd | ||
|
|
2aa88a2921 | ||
|
|
7fedb5ab83 | ||
|
|
0eb9a25687 | ||
|
|
330e88c270 | ||
|
|
407d059076 | ||
|
|
de3d0d8515 | ||
|
|
57ef52d6ff | ||
|
|
189bd0d749 | ||
|
|
4ef8d9e826 | ||
|
|
7bb8d28b3b | ||
|
|
94ebc08097 | ||
|
|
a3e14c57e4 | ||
|
|
b55bdf8f04 | ||
|
|
3e0a97dbe0 | ||
|
|
9b13a56002 | ||
|
|
32c145b02d | ||
|
|
25b359a136 | ||
|
|
0eb338a0bf | ||
|
|
b59a29cff4 | ||
|
|
c252e77c5b | ||
|
|
44ca712186 | ||
|
|
93cc945a2c | ||
|
|
68d6c39dd4 | ||
|
|
3a5a185940 | ||
|
|
3d351ece05 | ||
|
|
c65ed16790 | ||
|
|
7c147befcb | ||
|
|
c73c29d2ec | ||
|
|
df0c6b5fa8 | ||
|
|
2ae7ec33f1 | ||
|
|
65beeffa93 | ||
|
|
b5fa6192b3 | ||
|
|
721a3d2d27 | ||
|
|
bf9173301c | ||
|
|
e2cf6a40cb | ||
|
|
db681273e6 | ||
|
|
291fd5427b | ||
|
|
362f1da11f | ||
|
|
cc33c63501 | ||
|
|
6e54497492 | ||
|
|
9fde453ead | ||
|
|
7cd48f1c09 | ||
|
|
4c3d277fce | ||
|
|
0111626d80 | ||
|
|
0b83546db7 | ||
|
|
08ab8237e7 | ||
|
|
39ecfba3e4 | ||
|
|
ae5502b650 | ||
|
|
821d0b5147 | ||
|
|
b4ed1f35ce | ||
|
|
a468342187 | ||
|
|
4f6035dc49 | ||
|
|
506adcf81f | ||
|
|
49c8c2d57d | ||
|
|
2ab698c2d3 | ||
|
|
770f5f9724 | ||
|
|
992dcd651b | ||
|
|
98a38b03e4 | ||
|
|
626995ec91 | ||
|
|
00b0c8deaf | ||
|
|
8e5b51c42f | ||
|
|
b81ec0f073 | ||
|
|
d49d42254b | ||
|
|
95d5b4afa1 | ||
|
|
cca8c9bdf7 | ||
|
|
4e00191039 | ||
|
|
dd09595866 | ||
|
|
b27bfc3fa9 | ||
|
|
c6a9716d16 | ||
|
|
acd7a54ee9 | ||
|
|
e7d240549e | ||
|
|
70fedc53b9 | ||
|
|
bed8c9172d | ||
|
|
ef1ac3e5e9 | ||
|
|
cc163f952f | ||
|
|
95180ba963 | ||
|
|
56d7f0ef7b | ||
|
|
fbd6a5931e | ||
|
|
66dd3561cf | ||
|
|
58db3fdaa2 | ||
|
|
92b6738ff4 | ||
|
|
63f57667ba | ||
|
|
34718a7b89 | ||
|
|
6ac91e6cf3 | ||
|
|
a39d6ab346 | ||
|
|
91d89670d2 | ||
|
|
6a2992530e | ||
|
|
1f839a886d | ||
|
|
91f9a6756a | ||
|
|
89137c0873 | ||
|
|
ffa31c7aa3 | ||
|
|
e12bfdbe58 | ||
|
|
3c887d8392 | ||
|
|
b4d2b6b113 | ||
|
|
fe40c4b952 | ||
|
|
9319c74932 | ||
|
|
4c60b04057 | ||
|
|
1a24f62074 | ||
|
|
1824d38f75 | ||
|
|
e41f200bcd | ||
|
|
ccc990f854 | ||
|
|
56bde0e19b | ||
|
|
6b650e777f | ||
|
|
49a7fda932 | ||
|
|
8ff817856e | ||
|
|
3a85a05078 | ||
|
|
cda78c56c5 | ||
|
|
499f930086 | ||
|
|
393b815409 | ||
|
|
984010dc7a | ||
|
|
0bbc413423 | ||
|
|
98a90e3e1e | ||
|
|
3ccbece822 | ||
|
|
0f81233d4f | ||
|
|
f6b1f02d11 | ||
|
|
301476ff5c | ||
|
|
b84f44bf1e | ||
|
|
842afe6745 | ||
|
|
bc5fa68de8 | ||
|
|
fec2f3ff94 | ||
|
|
77718809ef | ||
|
|
9b328893f7 | ||
|
|
45378b8128 | ||
|
|
b830c6db1b | ||
|
|
7773a51f2a | ||
|
|
f68badb659 | ||
|
|
4ea8501fa9 | ||
|
|
1f86697d2d | ||
|
|
faf24e15bd | ||
|
|
592d482a9b | ||
|
|
ad05e375f1 | ||
|
|
8b30d451a4 | ||
|
|
abe78370ea | ||
|
|
00e2e90aa7 | ||
|
|
f37d70b58e | ||
|
|
09065c705b | ||
|
|
ad58960831 | ||
|
|
7ce74d378f | ||
|
|
8c0ba2dd1b | ||
|
|
016c64610e | ||
|
|
c578543e18 | ||
|
|
d247750e62 | ||
|
|
b8896ec1df | ||
|
|
4a69e9efb4 | ||
|
|
b43bac37db | ||
|
|
0c7b41747e | ||
|
|
8fda6937a5 | ||
|
|
0a4a933a2a | ||
|
|
866466d158 | ||
|
|
d89d5eac9c | ||
|
|
6885463b11 | ||
|
|
a273fdaa1d | ||
|
|
ec60e98b2e | ||
|
|
dc0f7c4668 | ||
|
|
ba7fe15a57 | ||
|
|
3d51b84188 | ||
|
|
f66cebb727 | ||
|
|
9e355c8e52 | ||
|
|
9ce0982191 | ||
|
|
f543802f9e | ||
|
|
32aecadf75 | ||
|
|
d39e3d7b90 | ||
|
|
bd66659b92 | ||
|
|
1a7a794dc9 | ||
|
|
2519633265 | ||
|
|
e38121efee | ||
|
|
261b036f75 | ||
|
|
7e857b53a7 | ||
|
|
73e1d72403 | ||
|
|
5731cccddc | ||
|
|
48d22fc06d | ||
|
|
964ed294a5 | ||
|
|
2a4bb902df | ||
|
|
d3463b82bc | ||
|
|
ef536a0c07 | ||
|
|
7e7c5317b1 | ||
|
|
2bf3546971 | ||
|
|
cd42b0dc65 | ||
|
|
4ba3caeaf4 | ||
|
|
10ac1af6b0 | ||
|
|
833e75ade1 | ||
|
|
7bbd80ed21 | ||
|
|
dbedc9d1d3 | ||
|
|
3bdcc25ebe | ||
|
|
d0108e9859 | ||
|
|
025c89d78c | ||
|
|
c52e60b410 | ||
|
|
3cc644193f | ||
|
|
8020792024 | ||
|
|
837a27f9d7 | ||
|
|
5c71f6254b | ||
|
|
bbea6c2ae4 | ||
|
|
fa799e9542 | ||
|
|
7f1ce1e645 | ||
|
|
dc6bf3795a | ||
|
|
a3538f95e2 | ||
|
|
85c358043d | ||
|
|
ab8f01284a | ||
|
|
97d824d779 | ||
|
|
eedf9d2935 | ||
|
|
1ede56e137 | ||
|
|
f8c106bcb9 | ||
|
|
10c206270a | ||
|
|
f7a0ee7174 | ||
|
|
f5e8ba96e6 | ||
|
|
3506fdaad9 | ||
|
|
8824b779f5 | ||
|
|
4cab92d615 | ||
|
|
35a14d3198 | ||
|
|
e46041ece0 | ||
|
|
28ebaf48c8 | ||
|
|
fc96e7919a | ||
|
|
08b80d2e72 | ||
|
|
051cf9370b | ||
|
|
3d13296890 | ||
|
|
93f32c2903 | ||
|
|
f168768033 | ||
|
|
5ab4307963 | ||
|
|
d0baeef064 | ||
|
|
44b83c888c | ||
|
|
da45fa1969 | ||
|
|
5ad3a10251 | ||
|
|
9ec94a472f | ||
|
|
4e4c39eea4 | ||
|
|
3be9aecb88 | ||
|
|
a6a5af781e | ||
|
|
a93ced6e34 | ||
|
|
82135c8b9d | ||
|
|
9f248e0802 | ||
|
|
65aa95504b | ||
|
|
def7c67614 | ||
|
|
be3f30e666 | ||
|
|
b920fe4b8c | ||
|
|
4b6b10526d | ||
|
|
74b2f8f398 | ||
|
|
34ef4b3fb4 | ||
|
|
63b0f6bb99 | ||
|
|
36973669ea | ||
|
|
2e301c5389 | ||
|
|
6a4b0f7bc5 | ||
|
|
8b063815ab | ||
|
|
75b79d3d4a | ||
|
|
b61b73685f | ||
|
|
864e3bd6f4 | ||
|
|
013e4ae28b | ||
|
|
51a0a16afb | ||
|
|
a5165e2345 | ||
|
|
c41a337791 | ||
|
|
e58dec35c0 | ||
|
|
5e7697039a | ||
|
|
45de9cea3a | ||
|
|
9f075e7bc6 | ||
|
|
a88d1cf11e | ||
|
|
a6eec88dcb | ||
|
|
3c4afe36e8 | ||
|
|
5b71df37ec | ||
|
|
d7d3d52a30 | ||
|
|
ac222c4946 | ||
|
|
0200287203 | ||
|
|
5677bd4112 | ||
|
|
cbe3697d68 | ||
|
|
445410fcd7 | ||
|
|
1d513441c6 | ||
|
|
0a31103f91 | ||
|
|
85bf4adc72 | ||
|
|
cc55a6dacb | ||
|
|
e099e2cacd | ||
|
|
658946ff70 | ||
|
|
b6a31ed789 | ||
|
|
9c272a0d4d | ||
|
|
3e626a37db | ||
|
|
e2150e3823 | ||
|
|
6480fef315 | ||
|
|
95df2ceb42 | ||
|
|
189698dddb | ||
|
|
438d5581aa | ||
|
|
7af78b11f5 | ||
|
|
5d68211ce9 | ||
|
|
a7e1a88dc8 | ||
|
|
3054032a37 | ||
|
|
79279630ba | ||
|
|
e019fb6db2 | ||
|
|
85bdecc573 | ||
|
|
21f3d93295 | ||
|
|
888e9d0e4a | ||
|
|
e6d84e6859 | ||
|
|
65371f18d4 | ||
|
|
1b5ab33c25 | ||
|
|
396dd0b169 | ||
|
|
eb40369634 | ||
|
|
0c87f06972 | ||
|
|
c774796422 | ||
|
|
c12c6ab107 | ||
|
|
e3b9b67fee | ||
|
|
2407b4b780 | ||
|
|
e962d91c77 | ||
|
|
d0afa2f61f | ||
|
|
58d13ba9e3 | ||
|
|
631ca76136 | ||
|
|
9cb9d47857 | ||
|
|
2323d89938 | ||
|
|
6769fefd00 | ||
|
|
5dbf1c7ce2 | ||
|
|
8d3c113475 | ||
|
|
087bf08d44 | ||
|
|
32b2ce280c | ||
|
|
37c9b033c5 | ||
|
|
f8d806999d | ||
|
|
86c4011797 | ||
|
|
bbc4c1d61c | ||
|
|
951e908d4b | ||
|
|
536929ff0e | ||
|
|
1f5a46b883 | ||
|
|
4cab47235b | ||
|
|
5524a77736 | ||
|
|
a3cdfe693d | ||
|
|
9e4ad1495d | ||
|
|
914da94c01 | ||
|
|
950dcce803 | ||
|
|
f82e480c32 | ||
|
|
9c5726cbca | ||
|
|
550e53f225 | ||
|
|
6e04ca054d | ||
|
|
b014d95005 | ||
|
|
0123b98e47 | ||
|
|
d7093487dd | ||
|
|
02c9a0cb1c | ||
|
|
eb29be587f | ||
|
|
f64a23d98f | ||
|
|
8bbadb9e57 | ||
|
|
4066cd83cc | ||
|
|
cca35bd5e5 | ||
|
|
187adfa738 | ||
|
|
246c03dea1 | ||
|
|
e68571a7a6 | ||
|
|
128c85fc0c | ||
|
|
c75b25c4b6 | ||
|
|
6abcfcea75 | ||
|
|
b300b72626 | ||
|
|
b2995955b6 | ||
|
|
1c9d07a47d | ||
|
|
66f99eb4d4 | ||
|
|
ff584a323d | ||
|
|
8c658ac930 | ||
|
|
588c1c07df | ||
|
|
f54639c791 | ||
|
|
7c78ddc929 | ||
|
|
9f3ccdc508 | ||
|
|
99c222cffa | ||
|
|
3556cd0e66 | ||
|
|
57e3574693 | ||
|
|
98c166a6b2 | ||
|
|
b6a31e798f | ||
|
|
5ebb714d05 | ||
|
|
9958f73926 | ||
|
|
0fb33744ae | ||
|
|
6d26d1beb0 | ||
|
|
0a8dd56306 | ||
|
|
8a8d714901 | ||
|
|
c2cb5a3044 | ||
|
|
9893b2f057 | ||
|
|
83b2f85ff4 | ||
|
|
47df8968c7 | ||
|
|
33b4b4b7ab | ||
|
|
21fd8fdd8d | ||
|
|
7ec8391872 | ||
|
|
afb9e59711 | ||
|
|
24a93ecc9f | ||
|
|
2de9b96138 | ||
|
|
e0d5aba94a | ||
|
|
f3b4433d5b | ||
|
|
f4678e2813 | ||
|
|
b958145761 | ||
|
|
feea9a2e67 | ||
|
|
ebb9d0be3f | ||
|
|
f5cbec21f4 | ||
|
|
e55d6111cf | ||
|
|
cb01fb21d3 | ||
|
|
c5be99e58d | ||
|
|
92458d0d3b | ||
|
|
5d835b05ee | ||
|
|
2a47fa4de5 | ||
|
|
ead17949f9 | ||
|
|
d5a848e79e | ||
|
|
553cb16bc4 | ||
|
|
47f8c9e648 | ||
|
|
791138ccab | ||
|
|
6cbef27cfb | ||
|
|
e5f7dce9b8 | ||
|
|
a415ef573e | ||
|
|
ffa2577cc2 | ||
|
|
2a0daae155 | ||
|
|
b41e328d22 | ||
|
|
1ac42d85d6 | ||
|
|
685a791aba | ||
|
|
89a1663e28 | ||
|
|
0563ac917a | ||
|
|
719ef3bc71 | ||
|
|
18fd88a441 | ||
|
|
6d93b78f9a | ||
|
|
4365a693f2 | ||
|
|
14fa493ac9 | ||
|
|
36e3dd559c | ||
|
|
b681d57b4e | ||
|
|
c5fa40166f | ||
|
|
b7cf234db6 | ||
|
|
f27dc54ec4 | ||
|
|
4c1dec613c | ||
|
|
a251685002 | ||
|
|
118221e3df | ||
|
|
1dd5f893da | ||
|
|
a46640f43d | ||
|
|
a293078603 | ||
|
|
f3ed79dc71 | ||
|
|
ee8fd2e9f2 | ||
|
|
8b9ec5f976 | ||
|
|
d283eae7a0 | ||
|
|
a854db760c | ||
|
|
caf2ec882a | ||
|
|
bbf967e2b7 | ||
|
|
b9d011b48e | ||
|
|
ecdadf5335 | ||
|
|
10b80442d0 | ||
|
|
a2879a4558 | ||
|
|
62025d1053 | ||
|
|
09fe550a28 | ||
|
|
07aac1e6a3 | ||
|
|
b8939b504e | ||
|
|
4f29d0c2e4 | ||
|
|
592c6683c3 | ||
|
|
f8edac72ea | ||
|
|
fc0e1150f6 | ||
|
|
e1fd913817 | ||
|
|
ede9f93a59 | ||
|
|
c6c0eb88e9 | ||
|
|
f929790be6 | ||
|
|
a4ff9d6d31 | ||
|
|
a21d28a91f | ||
|
|
f7c32bc28b | ||
|
|
59c0ca947d | ||
|
|
a7ac1adb80 | ||
|
|
17204cc026 | ||
|
|
cf4aecbd3e | ||
|
|
9baaf824c6 | ||
|
|
eb172fc54e | ||
|
|
e99762b814 | ||
|
|
6b1d4a60e3 | ||
|
|
a540b3cfee | ||
|
|
b0481d6b30 | ||
|
|
0c315c53e5 | ||
|
|
75c37a454f | ||
|
|
554d7e17be |
@@ -0,0 +1 @@
|
||||
custom: ['https://www.buymeacoffee.com/beemdevelopment']
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "Bug report"
|
||||
about: "Create a report to help us fix a bug"
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!-- Please read the [bug reports section of the contribution
|
||||
guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
|
||||
|
||||
###### Info
|
||||
|
||||
* __Version__:
|
||||
* __Source__: (Google Play/GitHub/F-Droid/?)
|
||||
* __Vault encrypted__: Yes (with biometric unlock)/Yes/No
|
||||
* __Device__:
|
||||
* __Android version and ROM__:
|
||||
|
||||
###### Steps to reproduce
|
||||
|
||||
A detailed list of reproduction steps.
|
||||
|
||||
###### What do you expect to happen?
|
||||
|
||||
...
|
||||
|
||||
###### What happens instead?
|
||||
|
||||
...
|
||||
|
||||
###### Log
|
||||
|
||||
```
|
||||
If applicable, paste the debug log here.
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: "Feature request"
|
||||
about: "Suggest a new feature for this project"
|
||||
labels: proposal
|
||||
---
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
name: build
|
||||
on: [pull_request, push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v2
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/wrapper-validation-action@e2c57acffb2c9aa5a8dc6eda2bbae0b6e495bd4c
|
||||
- name: Build the app
|
||||
run: ./gradlew build
|
||||
+3
-2
@@ -16,6 +16,7 @@ out/
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
release/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
@@ -34,8 +35,8 @@ captures/
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/libraries
|
||||
.idea/
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
crowdin.properties
|
||||
|
||||
Generated
-1
@@ -1 +0,0 @@
|
||||
Aegis
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<resourceExtensions />
|
||||
<wildcardResourcePatterns>
|
||||
<entry name="!?*.java" />
|
||||
<entry name="!?*.form" />
|
||||
<entry name="!?*.class" />
|
||||
<entry name="!?*.groovy" />
|
||||
<entry name="!?*.scala" />
|
||||
<entry name="!?*.flex" />
|
||||
<entry name="!?*.kt" />
|
||||
<entry name="!?*.clj" />
|
||||
<entry name="!?*.aj" />
|
||||
</wildcardResourcePatterns>
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="false">
|
||||
<processorPath useClasspath="true" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-3
@@ -1,3 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<compositeConfiguration>
|
||||
<compositeBuild compositeDefinitionSource="SCRIPT" />
|
||||
</compositeConfiguration>
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="F:\Android\Android Studio\gradle\gradle-2.14.1" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-33
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-9
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/Aegis.iml" filepath="$PROJECT_DIR$/Aegis.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# Contributing
|
||||
|
||||
Looking to contribute to Aegis? That's great! There are a couple of ways to help
|
||||
out. This document contains some general guidelines for each type of
|
||||
contribution.
|
||||
|
||||
## Translations
|
||||
|
||||
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
|
||||
translations of Aegis for lots of different languages. __Pull requests that
|
||||
add/update a translation are no longer accepted.__ Crowdin is our single source
|
||||
of truth for translations, to keep things easy to maintain.
|
||||
|
||||
The top 30 languages are available for translation. It's possible that yours is
|
||||
not in that list. If that's the case, feel free to send us an email and we'll
|
||||
add it.
|
||||
|
||||
## Pull requests
|
||||
|
||||
If you're planning on adding a new feature or making other large changes, please
|
||||
discuss it with us first through [a
|
||||
proposal](https://github.com/beemdevelopment/Aegis/issues/new?labels=proposal&template=feature.md)
|
||||
on GitHub. Discussing your idea with us first ensures that everyone is on the
|
||||
same page before you start working on your change. We don't like rejecting pull
|
||||
requests.
|
||||
|
||||
## Bug reports
|
||||
|
||||
We use GitHub's issue tracker to track bugs. To make bug reports easier to
|
||||
follow up on for us, they must follow [the
|
||||
template](.github/ISSUE_TEMPLATE/bug.md). If a bug report does not follow the
|
||||
template and does not contain enough information, it will be closed. Duplicate
|
||||
bug reports receive the same treatment.
|
||||
|
||||
Please consider trying to find the root cause yourself first and include your
|
||||
analysis of the issue in your report. Perhaps even send us a patch that fixes
|
||||
it! We're happy to help you if you get stuck along the way.
|
||||
|
||||
### Capturing a log with ADB
|
||||
|
||||
In some cases, we ask our users to obtain a debug log from their device. This is
|
||||
typically only necessary if Aegis:
|
||||
- Is unable to recover from an error and crashes.
|
||||
- Only shows a generic error to the user, but writes a more detailed one to the
|
||||
log.
|
||||
|
||||
Capturing a log with the Android Debug Bridge (ADB) allows us to see the stack
|
||||
trace and the exception that occurred.
|
||||
|
||||
#### Preparation
|
||||
|
||||
Before you can capture a log, you first need to go through a one-time setup
|
||||
process on your Android device and computer.
|
||||
|
||||
##### Prerequisites
|
||||
|
||||
- Your Android device.
|
||||
- A computer with Windows, Mac or Linux.
|
||||
- A USB cable to connect your Android device to your computer.
|
||||
|
||||
##### Setup
|
||||
|
||||
__On your Android device__:
|
||||
|
||||
1. Navigate to ``Settings -> About``, scroll down and start tapping on the build
|
||||
number until developer options are enabled.
|
||||
2. Navigate to ``Settings -> System -> Developer options`` and enable ``USB
|
||||
debugging``.
|
||||
|
||||
These navigation steps may differ slightly across Android versions and ROMs.
|
||||
|
||||
__On your computer__:
|
||||
|
||||
3. Download and extract the SDK platform tools for Android:
|
||||
https://developer.android.com/studio/releases/platform-tools.
|
||||
4. Start your terminal emulator (If you're on Windows, start PowerShell) and
|
||||
navigate to the folder where platform-tools was extracted.
|
||||
5. Execute ``adb devices``.
|
||||
|
||||
__On your Android device__:
|
||||
|
||||
6. A prompt will appear. Select "Always allow from this computer" and accept the
|
||||
connection.
|
||||
|
||||
#### Capturing a log
|
||||
|
||||
__On your Android device__:
|
||||
|
||||
1. Start Aegis.
|
||||
|
||||
__On your PC__:
|
||||
|
||||
2. Start your terminal emulator (If you're on Windows, start PowerShell) and
|
||||
navigate to the folder where platform-tools was extracted.
|
||||
3. Start a log capture by executing the following commands.
|
||||
|
||||
```
|
||||
adb logcat -c
|
||||
adb logcat > debug.log
|
||||
```
|
||||
|
||||
The logcat command captures the full system log by default, which may expose
|
||||
some sensitive information. While this information can sometimes help with
|
||||
finding the root cause of the issue, it is not always necessary. To only
|
||||
capture the log output of Aegis, replace the last logcat command with the
|
||||
one below:
|
||||
|
||||
```sh
|
||||
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) > debug.log
|
||||
```
|
||||
|
||||
_If you are using a debug APK, replace ``com.beemdevelopment.aegis`` with
|
||||
``com.beemdevelopment.aegis.debug``._
|
||||
|
||||
__On your Android device__:
|
||||
|
||||
4. Reproduce the issue.
|
||||
|
||||
__On your PC__:
|
||||
|
||||
5. Stop the log capture with Ctrl+C.
|
||||
6. Attach the ``debug.log`` file to your issue on GitHub.
|
||||
@@ -1,64 +1,140 @@
|
||||
# Aegis
|
||||
<img align="left" width="80" height="80" src="metadata/en-US/images/icon.png"
|
||||
alt="App icon">
|
||||
|
||||
Aegis is a free, secure and open source 2FA app for Android.
|
||||
# Aegis Authenticator [](https://github.com/beemdevelopment/Aegis/actions?query=workflow%3Abuild) [](https://crowdin.com/project/aegis-authenticator) [](https://www.buymeacoffee.com/beemdevelopment) [](https://matrix.to/#/#aegis:matrix.org)
|
||||
|
||||
# Features
|
||||
__Aegis Authenticator__ is a free, secure and open source 2FA app for Android.
|
||||
It aims to provide a secure authenticator for your online services, while also
|
||||
including some features missing in existing authenticator apps, like proper
|
||||
encryption and backups. Aegis supports HOTP and TOTP, making it compatible with
|
||||
thousands of services.
|
||||
|
||||
The security design of the app and the vault format is described in detail in
|
||||
[this document](docs/vault.md).
|
||||
|
||||
## Features
|
||||
|
||||
- Free and open source
|
||||
- Secure
|
||||
- Encryption (AES-256)
|
||||
- The vault is encrypted (AES-256-GCM), and can be unlocked with:
|
||||
- Password (scrypt)
|
||||
- Fingerprint (Android Keystore)
|
||||
- Biometrics (Android Keystore)
|
||||
- Screen capture prevention
|
||||
- Tap to reveal ability
|
||||
- Multiple ways to add new entries
|
||||
- Scan QR code
|
||||
- Enter details manually
|
||||
- Import
|
||||
- andOTP
|
||||
- FreeOTP
|
||||
- Supported algorithms:
|
||||
- HOTP ([RFC 4226](https://tools.ietf.org/html/rfc4226))
|
||||
- TOTP ([RFC 6238](https://tools.ietf.org/html/rfc6238))
|
||||
- Tap to reveal
|
||||
- Compatible with Google Authenticator
|
||||
- Supports industry standard algorithms:
|
||||
[HOTP](https://tools.ietf.org/html/rfc4226) and
|
||||
[TOTP](https://tools.ietf.org/html/rfc6238)
|
||||
- Lots of ways to add new entries
|
||||
- Scan a QR code or an image of one
|
||||
- Enter details manually
|
||||
- Import from other authenticator apps: 2FAS Authenticator, Authenticator
|
||||
Plus, Authy, andOTP, FreeOTP, FreeOTP+, Google Authenticator, Microsoft
|
||||
Authenticator, Plain text, Steam, TOTP Authenticator and WinAuth (root
|
||||
access is required for some of these)
|
||||
- Organization
|
||||
- Custom or default icons
|
||||
- Drag and drop
|
||||
- Custom groups
|
||||
- Alphabetic/custom sorting
|
||||
- Custom or automatically generated icons
|
||||
- Group entries together
|
||||
- Advanced entry editing
|
||||
- Material design
|
||||
- Light and dark themes
|
||||
- Search by name/issuer
|
||||
- Material design with multiple themes: Light, Dark, AMOLED
|
||||
- Export (plaintext or encrypted)
|
||||
- Automatic backups of the vault to a location of your choosing
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
|
||||
[<img width=200 alt="Settings Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
|
||||
[<img width=200 alt="Edit Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true).png
|
||||
[<img width=200 alt="Screenshot 1"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 2"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 3"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true)
|
||||
|
||||
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true)
|
||||
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 4"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 5"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true)
|
||||
[<img width=200 alt="Screenshot 6"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true)
|
||||
|
||||
[<img width=200 alt="Screenshot 7"
|
||||
src="metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true)
|
||||
|
||||
## Downloads
|
||||
|
||||
Aegis is available in the Google Play Store.
|
||||
Aegis is available on the Google Play Store and on F-Droid.
|
||||
|
||||
[<img height=80 alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" />](http://play.google.com/store/apps/details?id=com.beemdevelopment.aegis)
|
||||
[<img height=80 alt="Get it on Google Play"
|
||||
src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
|
||||
/>](http://play.google.com/store/apps/details?id=com.beemdevelopment.aegis)
|
||||
[<img height="80" alt="Get it on F-Droid"
|
||||
src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
/>](https://f-droid.org/app/com.beemdevelopment.aegis)
|
||||
|
||||
### Verification
|
||||
|
||||
There are plans to make it available on F-Droid as well.
|
||||
APK releases on Google Play and GitHub are signed using the same key. They can
|
||||
be verified using
|
||||
[apksigner](https://developer.android.com/studio/command-line/apksigner.html#options-verify):
|
||||
|
||||
## Libraries
|
||||
```
|
||||
apksigner verify --print-certs --verbose aegis.apk
|
||||
```
|
||||
|
||||
- [TextDrawable](https://github.com/amulyakhare/TextDrawable) by Amulya Khare
|
||||
- [FloatingActionButton](https://github.com/Clans/FloatingActionButton) by Dmytro Tarianyk
|
||||
- [AppIntro](https://github.com/AppIntro/AppIntro) by Paolo Rotolo
|
||||
- [Krop](https://github.com/avito-tech/krop) by Avito Technology
|
||||
- [SpongyCastle](https://github.com/rtyley/spongycastle) by Roberto Tyley
|
||||
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
|
||||
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning Dodenhof
|
||||
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
|
||||
- [libsu](https://github.com/topjohnwu/libsu) by John Wu
|
||||
The output should look like:
|
||||
|
||||
```
|
||||
Verifies
|
||||
Verified using v1 scheme (JAR signing): true
|
||||
Verified using v2 scheme (APK Signature Scheme v2): true
|
||||
```
|
||||
|
||||
The certificate fingerprints should correspond to the ones listed below:
|
||||
|
||||
```
|
||||
Owner: CN=Beem Development
|
||||
Issuer: CN=Beem Development
|
||||
Serial number: 172380c
|
||||
Valid from: Sat Feb 09 14:05:49 CET 2019 until: Wed Feb 03 14:05:49 CET 2044
|
||||
Certificate fingerprints:
|
||||
MD5: AA:EE:86:DB:C7:B8:88:9F:1F:C9:D0:7A:EC:37:36:32
|
||||
SHA1: 59:FB:63:B7:1F:CE:95:74:6C:EB:1E:1A:CB:2C:2E:45:E5:FF:13:50
|
||||
SHA256: C6:DB:80:A8:E1:4E:52:30:C1:DE:84:15:EF:82:0D:13:DC:90:1D:8F:E3:3C:F3:AC:B5:7B:68:62:D8:58:A8:23
|
||||
```
|
||||
|
||||
### Icon packs
|
||||
|
||||
Aegis supports icon packs to make it easier to assign icons to the entries in
|
||||
your vault. There are no official icon packs, but the community maintains a
|
||||
number of third-party icon packs you may want to check out. To learn how to
|
||||
create your own Aegis-compatible icon pack, see [the
|
||||
documenation](docs/iconpacks.md).
|
||||
|
||||
- [aegis-icons](https://github.com/aegis-icons/aegis-icons)
|
||||
|
||||
Unofficial monochrome-styled 2FA icons.
|
||||
|
||||
[<img width=500 alt="aegis-icons preview"
|
||||
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
|
||||
|
||||
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons)
|
||||
|
||||
This project periodically generates an icon pack for Aegis based on [Simple
|
||||
Icons](https://simpleicons.org/). The icons are automatically generated, so
|
||||
not all of them are as high quality as the ones you'll find in
|
||||
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
|
||||
|
||||
## Contributing
|
||||
|
||||
Looking to contribute to Aegis? That's great! There are a couple of ways to help
|
||||
out. Translations, bug reports and pull requests are all greatly appreciated.
|
||||
Please refer to our [contributing guidelines](CONTRIBUTING.md) to get started.
|
||||
|
||||
Swing by our Matrix room to interact with other contributors:
|
||||
[#aegis:matrix.org](https://matrix.to/#/#aegis:matrix.org).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
|
||||
This project is licensed under the GNU General Public License v3.0. See the
|
||||
[LICENSE](LICENSE) file for details.
|
||||
|
||||
+163
-27
@@ -1,52 +1,188 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
|
||||
def getCmdOutput = { cmd ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine cmd
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
|
||||
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
|
||||
|
||||
def packageName = "com.beemdevelopment.aegis"
|
||||
def fileProviderAuthority = "${packageName}.fileprovider"
|
||||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 3
|
||||
versionName "0.2"
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 49
|
||||
versionName "2.0.1"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
|
||||
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
disable "MissingTranslation"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests {
|
||||
all {
|
||||
maxHeapSize "3g"
|
||||
|
||||
ignoreFailures false
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
|
||||
showExceptions true
|
||||
exceptionFormat "full"
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
}
|
||||
}
|
||||
|
||||
includeAndroidResources true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders = [title:"AegisDev", iconName:"ic_launcher_debug"]
|
||||
manifestPlaceholders = [
|
||||
title: "AegisDev",
|
||||
iconName: "ic_launcher_debug",
|
||||
fileProviderAuthority: "${fileProviderAuthorityDebug}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "false"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
removeUnusedResources true
|
||||
obfuscate false
|
||||
optimizeCode false
|
||||
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
manifestPlaceholders = [title:"Aegis", iconName:"ic_launcher"]
|
||||
manifestPlaceholders = [
|
||||
title: "Aegis",
|
||||
iconName: "ic_launcher",
|
||||
fileProviderAuthority: "${fileProviderAuthority}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "true"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
removeUnusedResources true
|
||||
obfuscate false
|
||||
optimizeCode true
|
||||
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility 1.8
|
||||
sourceCompatibility 1.8
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.8.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def libsuVersion = '2.3.2'
|
||||
def androidTestVersion = '1.3.0'
|
||||
def cameraxVersion = '1.0.0'
|
||||
def glideVersion = '4.12.0'
|
||||
def guavaVersion = '30.1.1'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '3.1.2'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.2.0'
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha24"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.getbase:floatingactionbutton:1.10.1'
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.2'
|
||||
implementation 'com.github.avito-tech:krop:3e65e12'
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
implementation 'com.mattprecious.swirl:swirl:1.0.0'
|
||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9'
|
||||
implementation "androidx.core:core:1.3.2"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.3.1"
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'com.github.avito-tech:krop:0.52'
|
||||
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
|
||||
implementation "com.github.bumptech.glide:glide:${glideVersion}"
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
|
||||
transitive = false
|
||||
}
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.16.0'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.5.0'
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
// NOTE: this is kept at an old version on purpose (something in newer versions breaks the Authenticator Plus importer)
|
||||
implementation 'net.lingala.zip4j:zip4j:2.6.0'
|
||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
|
||||
|
||||
androidTestImplementation "androidx.test:core:${androidTestVersion}"
|
||||
androidTestImplementation "androidx.test:runner:${androidTestVersion}"
|
||||
androidTestImplementation "androidx.test:rules:${androidTestVersion}"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
|
||||
androidTestImplementation "junit:junit:${junitVersion}"
|
||||
androidTestUtil 'androidx.test:orchestrator:1.3.0'
|
||||
|
||||
testImplementation "androidx.test:core:${androidTestVersion}"
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "junit:junit:${junitVersion}"
|
||||
testImplementation "org.json:json:20210307"
|
||||
testImplementation 'org.robolectric:robolectric:4.5.1'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore regexp="X509LDAPCertStoreSpi" />
|
||||
</issue>
|
||||
</lint>
|
||||
Vendored
+8
@@ -15,3 +15,11 @@
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
|
||||
|
||||
-keep class com.beemdevelopment.aegis.ui.fragments.*
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
-keep class !org.bouncycastle.jce.provider.X509LDAPCertStoreSpi { *; }
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public abstract class AegisTest {
|
||||
public static final String VAULT_PASSWORD = "test";
|
||||
|
||||
protected AegisApplication getApp() {
|
||||
return (AegisApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
|
||||
}
|
||||
|
||||
protected VaultManager getVault() {
|
||||
return getApp().getVaultManager();
|
||||
}
|
||||
|
||||
protected VaultManager initVault() {
|
||||
VaultFileCredentials creds = generateCredentials();
|
||||
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
|
||||
try {
|
||||
vault.save(false);
|
||||
} catch (VaultManagerException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
getApp().getPreferences().setIntroDone(true);
|
||||
return vault;
|
||||
}
|
||||
|
||||
protected VaultFileCredentials generateCredentials() {
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
byte[] salt = CryptoUtils.generateSalt();
|
||||
SCryptParameters scryptParams = new SCryptParameters(
|
||||
CryptoUtils.CRYPTO_SCRYPT_N,
|
||||
CryptoUtils.CRYPTO_SCRYPT_r,
|
||||
CryptoUtils.CRYPTO_SCRYPT_p,
|
||||
salt
|
||||
);
|
||||
|
||||
VaultFileCredentials creds = new VaultFileCredentials();
|
||||
try {
|
||||
SecretKey key = slot.deriveKey(VAULT_PASSWORD.toCharArray(), scryptParams);
|
||||
slot.setKey(creds.getKey(), CryptoUtils.createEncryptCipher(key));
|
||||
} catch (NoSuchAlgorithmException
|
||||
| InvalidKeyException
|
||||
| InvalidAlgorithmParameterException
|
||||
| NoSuchPaddingException
|
||||
| SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
creds.getSlots().add(slot);
|
||||
return creds;
|
||||
}
|
||||
|
||||
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
|
||||
byte[] secret = CryptoUtils.generateRandomBytes(20);
|
||||
|
||||
OtpInfo info;
|
||||
try {
|
||||
info = type.getConstructor(byte[].class).newInstance(secret);
|
||||
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
}
|
||||
|
||||
// source: https://stackoverflow.com/a/30338665
|
||||
protected static ViewAction clickChildViewWithId(final int id) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Click on a child view with specified id.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
View v = view.findViewById(id);
|
||||
v.performClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnitRunner;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
public class AegisTestRunner extends AndroidJUnitRunner {
|
||||
@Override
|
||||
public void callApplicationOnCreate(Application app) {
|
||||
Context context = app.getApplicationContext();
|
||||
|
||||
// clear internal storage so that there is no vault file
|
||||
IOUtils.clearDirectory(context.getFilesDir(), false);
|
||||
|
||||
// clear preferences so that the intro is started from MainActivity
|
||||
ApplicationProvider.getApplicationContext().getFilesDir();
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.clear()
|
||||
.apply();
|
||||
|
||||
super.callApplicationOnCreate(app);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||
public ApplicationTest() {
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class IntentTest extends AegisTest {
|
||||
@Before
|
||||
public void before() {
|
||||
initVault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent() {
|
||||
VaultEntry entry = generateEntry(TotpInfo.class, "Bob", "Google");
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
launch(info.getUri());
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
|
||||
VaultEntry createdEntry = (VaultEntry) getVault().getEntries().toArray()[0];
|
||||
assertTrue(createdEntry.equivalates(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent_Empty() {
|
||||
launch(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent_Bad() {
|
||||
launch(Uri.parse("otpauth://bad"));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void launch(Uri uri) {
|
||||
Intent intent = new Intent(getApp(), MainActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(uri);
|
||||
|
||||
// we need to use the deprecated ActivityTestRule class because of https://github.com/android/android-test/issues/143
|
||||
ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
|
||||
rule.launchActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.IntroActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class IntroTest extends AegisTest {
|
||||
@Rule
|
||||
public final ActivityScenarioRule<IntroActivity> activityRule = new ActivityScenarioRule<>(IntroActivity.class);
|
||||
|
||||
@Test
|
||||
public void doIntro_None() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
ViewInteraction prev = onView(withId(R.id.btnPrevious));
|
||||
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_none)).perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(getVault().getCredentials());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doIntro_Password() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
ViewInteraction prev = onView(withId(R.id.btnPrevious));
|
||||
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD + "1"), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password_confirm)).perform(replaceText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
prev.perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
SlotList slots = getVault().getCredentials().getSlots();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(slots.has(PasswordSlot.class));
|
||||
assertFalse(slots.has(BiometricSlot.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.espresso.matcher.RootMatchers;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class OverallTest extends AegisTest {
|
||||
private static final String _groupName = "Test";
|
||||
|
||||
@Rule
|
||||
public final ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class);
|
||||
|
||||
@Test
|
||||
public void doOverallTest() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
|
||||
List<VaultEntry> entries = Arrays.asList(
|
||||
generateEntry(TotpInfo.class, "Frank", "Google"),
|
||||
generateEntry(HotpInfo.class, "John", "GitHub"),
|
||||
generateEntry(TotpInfo.class, "Alice", "Office 365"),
|
||||
generateEntry(SteamInfo.class, "Gaben", "Steam")
|
||||
);
|
||||
for (VaultEntry entry : entries) {
|
||||
addEntry(entry);
|
||||
}
|
||||
|
||||
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
|
||||
for (int i = 0; i < realEntries.size(); i++) {
|
||||
String message = String.format("%s != %s", realEntries.get(i).toJson().toString(), entries.get(i).toJson().toString());
|
||||
assertTrue(message, realEntries.get(i).equivalates(entries.get(i)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
|
||||
}
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(R.id.action_copy)).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.action_edit)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
|
||||
onView(withId(R.id.dropdown_group)).perform(click());
|
||||
onView(withText(R.string.new_group)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
changeSort(R.string.sort_alphabetically_name);
|
||||
changeSort(R.string.sort_alphabetically_name_reverse);
|
||||
changeSort(R.string.sort_alphabetically);
|
||||
changeSort(R.string.sort_alphabetically_reverse);
|
||||
changeSort(R.string.sort_custom);
|
||||
|
||||
changeGroupFilter(_groupName);
|
||||
changeGroupFilter(null);
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
|
||||
onView(withId(R.id.action_share_qr)).perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
|
||||
onView(withId(R.id.action_delete)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.lock)).perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.button_decrypt)).perform(click());
|
||||
vault = getVault();
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.action_settings)).perform(click());
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
}
|
||||
|
||||
private void changeSort(@IdRes int resId) {
|
||||
onView(withId(R.id.action_sort)).perform(click());
|
||||
onView(withText(resId)).perform(click());
|
||||
}
|
||||
|
||||
private void changeGroupFilter(String text) {
|
||||
onView(withId(R.id.chip_group)).perform(click());
|
||||
if (text == null) {
|
||||
onView(withId(R.id.btnClear)).perform(click());
|
||||
} else {
|
||||
onView(withText(text)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
}
|
||||
}
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
onView(withId(R.id.fab)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
|
||||
onView(withId(R.id.accordian_header)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
|
||||
|
||||
if (entry.getInfo().getClass() != TotpInfo.class) {
|
||||
String otpType;
|
||||
if (entry.getInfo() instanceof HotpInfo) {
|
||||
otpType = "HOTP";
|
||||
} else if (entry.getInfo() instanceof SteamInfo) {
|
||||
otpType = "Steam";
|
||||
} else if (entry.getInfo() instanceof TotpInfo) {
|
||||
otpType = "TOTP";
|
||||
} else {
|
||||
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
|
||||
}
|
||||
|
||||
onView(withId(R.id.dropdown_type)).perform(click());
|
||||
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
}
|
||||
|
||||
String secret = Base32.encode(entry.getInfo().getSecret());
|
||||
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisTest;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class VaultManagerTest extends AegisTest {
|
||||
@Before
|
||||
public void before() {
|
||||
initVault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToggleEncryption() throws VaultManagerException {
|
||||
getVault().disableEncryption();
|
||||
assertFalse(getVault().isEncryptionEnabled());
|
||||
assertNull(getVault().getCredentials());
|
||||
|
||||
VaultFileCredentials creds = generateCredentials();
|
||||
getVault().enableEncryption(creds);
|
||||
assertTrue(getVault().isEncryptionEnabled());
|
||||
assertNotNull(getVault().getCredentials());
|
||||
assertEquals(getVault().getCredentials().getSlots().findAll(PasswordSlot.class).size(), 1);
|
||||
}
|
||||
}
|
||||
@@ -3,61 +3,117 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.beemdevelopment.aegis">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name="com.beemdevelopment.aegis.AegisApplication"
|
||||
android:name=".AegisApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupOnly="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:backupAgent=".AegisBackupAgent"
|
||||
android:icon="@mipmap/${iconName}"
|
||||
android:label="Aegis"
|
||||
android:supportsRtl="true"
|
||||
tools:replace="android:theme"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
android:theme="@style/Theme.Aegis.Launch"
|
||||
tools:replace="android:theme">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
<activity
|
||||
android:name="com.beemdevelopment.aegis.ui.MainActivity"
|
||||
android:label="${title}"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:launchMode="singleTask">
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about" />
|
||||
<activity
|
||||
android:name=".ui.ImportEntriesActivity"
|
||||
android:label="@string/title_activity_import_entries" />
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="${title}">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="otpauth" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.beemdevelopment.aegis.ui.ScannerActivity"
|
||||
android:label="Scan a QR code"
|
||||
android:theme="@style/AppTheme.Fullscreen"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize">
|
||||
</activity>
|
||||
android:name=".ui.ScannerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/title_activity_scan_qr"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.beemdevelopment.aegis.ui.EditEntryActivity"
|
||||
android:label="Edit profile"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
</activity>
|
||||
android:name=".ui.EditEntryActivity"
|
||||
android:label="@string/title_activity_edit_entry" />
|
||||
<activity
|
||||
android:name="com.beemdevelopment.aegis.ui.IntroActivity"
|
||||
android:theme="@style/Theme.Intro"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
android:name=".ui.IntroActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.beemdevelopment.aegis.ui.AuthActivity"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
<activity android:name="com.beemdevelopment.aegis.ui.PreferencesActivity">
|
||||
</activity>
|
||||
android:name=".ui.AuthActivity" />
|
||||
<activity
|
||||
android:name="com.beemdevelopment.aegis.ui.SlotManagerActivity"
|
||||
android:label="Manage key slots"
|
||||
android:theme="@style/AppTheme.TransparentActionBar">
|
||||
</activity>
|
||||
android:name=".ui.PreferencesActivity"
|
||||
android:label="@string/title_activity_preferences" />
|
||||
<activity
|
||||
android:name="com.beemdevelopment.aegis.ui.GroupManagerActivity"
|
||||
android:label="Manage groups"
|
||||
android:theme="@style/AppTheme.TransparentActionBar">
|
||||
android:name=".ui.SlotManagerActivity"
|
||||
android:label="@string/title_activity_manage_slots" />
|
||||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="@string/title_activity_manage_groups" />
|
||||
<activity
|
||||
android:name=".ui.PanicResponderActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.ExitActivity" />
|
||||
|
||||
<service android:name=".services.NotificationService" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${fileProviderAuthority}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths">
|
||||
</meta-data>
|
||||
</provider>
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<package android:name="com.authy.authy" />
|
||||
<package android:name="org.fedorahosted.freeotp" />
|
||||
<package android:name="org.liberty.android.freeotpplus" />
|
||||
<package android:name="com.google.android.apps.authenticator2" />
|
||||
<package android:name="com.azure.authenticator" />
|
||||
<package android:name="com.valvesoftware.android.steam.community" />
|
||||
<package android:name="com.authenticator.authservice2" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../../LICENSE
|
||||
@@ -0,0 +1,258 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
* {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
background-color: %1$s;
|
||||
color: %2$s;
|
||||
}
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding: 0;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
li {
|
||||
padding-bottom: 8px;
|
||||
list-style-position: outside;
|
||||
margin-left: 1em;
|
||||
}
|
||||
h4 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 7px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 2.0.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for sorting on most used tokens</li>
|
||||
<li>Some minor UX and stability improvements</li>
|
||||
<li>New translation: Vietnamese</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>QR code information was decoded incorrectly in some cases if the app was set to a certain language (Turkish, for example)</li>
|
||||
</ul>
|
||||
<h3>Version 2.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for icon packs</li>
|
||||
<li>Support for participation in Android's backup system (Google Drive, Seedvault)</li>
|
||||
<li>UI refresh (switched to the Material Components theme)</li>
|
||||
<li>Bottom sheet with chips to filter on groups</li>
|
||||
<li>Support for importing from 2FAS Authenticator</li>
|
||||
<li>Search in account names by default (and remove the setting)</li>
|
||||
<li>Replaced the FAB with a bottom sheet dialog</li>
|
||||
<li>Reorganization of settings into separate categories</li>
|
||||
<li>Ability to 'share' images of QR codes to scan in Aegis</li>
|
||||
<li>Option to save the current group filter</li>
|
||||
<li>New translations for Bulgarian, Danish, Latvian, Swedish and Ukranian</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The QR code scanner had trouble detecting QR codes on some devices due to low resolution image capture</li>
|
||||
<li>The app would vanish from the recent apps list after locking</li>
|
||||
<li>When importing from Nextcloud, Aegis would report that the file could not be found.</li>
|
||||
<li>The biometrics prompt would not appear on some devices</li>
|
||||
<li>The app would lock when selecting a file/icon on certain devices and configurations</li>
|
||||
<li>There were multiple layout issues on small screen devices</li>
|
||||
<li>Various other usability, performance and stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 1.4.2</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The app would crash if DocumentsUI is not present on the device</li>
|
||||
<li>The app would close when selecting an icon if auto lock on minimize was enabled</li>
|
||||
<li>Importing from Authy was flaky for entries that have an icon</li>
|
||||
<li>The dark theme was not properly applied to the QR code scanner view</li>
|
||||
<li>The app would crash on plain text export on some devices</li>
|
||||
<li>Importing from Authenticator Plus stopped working</li>
|
||||
</ul>
|
||||
<h3>Version 1.4.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Scanning QR codes stopped working on certain devices (primarily OnePlus)</li>
|
||||
</ul>
|
||||
<h3>Version 1.4</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Optionally delete the vault if a panic trigger is received from Ripple</li>
|
||||
<li>More customizable auto-lock</li>
|
||||
<li>More flexible export options
|
||||
<ul>
|
||||
<li>Share mechanism</li>
|
||||
<li>Offer to encrypt even if this feature is disabled in the app</li>
|
||||
<li>Export to a Google Authenticator URI file</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Perform exports/backups on a background thread (automatic backups now work with Nextcloud)</li>
|
||||
<li>Color improvements to the dark theme (slightly darker)</li>
|
||||
<li>Offer more locations to select an image/icon from</li>
|
||||
<li>Display some helpful information when importing from a different app</li>
|
||||
<li>Minimum tap to reveal timeout changed to 1 second</li>
|
||||
<li>After an entry is added, scroll to it and highlight it</li>
|
||||
<li>Updated translations, and new translations for: Basque, Chinese Traditional, Hindi, Indonesian, Japanese, Persian, Romanian, Slovak</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Scanning large images for QR codes would fail</li>
|
||||
<li>The FAB would remain hidden under certain circumstances</li>
|
||||
<li>The app would crash if an entry was added to the vault twice due to an IO error</li>
|
||||
<li>The app would crash if the device was rotated while a progress dialog was shown</li>
|
||||
<li>The PIN keyboard would show even if a new non-digit password was set</li>
|
||||
<li>The password reminder popup would be occluded by the autofill popup</li>
|
||||
<li>Importing from other apps on Android 11 was broken due to some permission issues</li>
|
||||
</ul>
|
||||
<h3>Version 1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Completely rewritten intro/onboarding</li>
|
||||
<li>Option to show a PIN keyboard when unlocking Aegis</li>
|
||||
<li>A password strength meter when setting up encryption (based on zxcvbn)</li>
|
||||
<li>RTL support</li>
|
||||
<li>Arabic and Portuguese translations</li>
|
||||
<li>Updates to existing translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Better lifecycle handling of the biometric authentication prompt</li>
|
||||
<li>The filename of exported vaults had a double .json extension</li>
|
||||
<li>The navigation bar color was incorrect on devices pre API 27</li>
|
||||
<li>QR code scanner performance and stability improvements</li>
|
||||
<li>Various other small usability and stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 1.2.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Fix a rare issue where the intro could end up in a bad state</li>
|
||||
</ul>
|
||||
<h3>Version 1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Add navigation bar color to themes</li>
|
||||
<li>Add support for importing from TOTP Authenticator</li>
|
||||
<li>Add support for importing from Microsoft Authenticator</li>
|
||||
<li>Add support for importing from Authenticator Plus</li>
|
||||
<li>Add support for importing a plain text Google Authenticator URI file</li>
|
||||
<li>Add support for importing from the new Google Authenticator export QR codes</li>
|
||||
<li>Add support for otpauth://steam URI's</li>
|
||||
<li>Add an option to copy tokens on tap (and disable it by default)</li>
|
||||
<li>Improve method to notify users on copy</li>
|
||||
<li>Add support for backups</li>
|
||||
<li>Improve multiselect flow</li>
|
||||
<li>Automatically adapt to system theme</li>
|
||||
<li>Add setting to change from 3 digit group size to 2 digit group size</li>
|
||||
<li>Use most frequent period to show progress</li>
|
||||
<li>Append a timestamp to the filename of exported vaults</li>
|
||||
<li>Add Hungarian translation</li>
|
||||
<li>Add Turkish translation</li>
|
||||
<li>Display a warning if automatic time sync is not enabled</li>
|
||||
<li>Minor card entry layout overhaul</li>
|
||||
<li>Ability to transfer tokens with qr codes</li>
|
||||
<li>Lockscreen overhaul</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Improve overall exception handling and error feedback to the user</li>
|
||||
<li>Improve icon editing flow</li>
|
||||
<li>Protect writes of the vault file against corruption with AtomicFile</li>
|
||||
<li>Make the parsing logic of the QR code URI more robust</li>
|
||||
<li>Importing from Authy now asks for password if needed</li>
|
||||
<li>Update Russian localization</li>
|
||||
<li>Increase password reminder period to 30 days</li>
|
||||
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
|
||||
<li>Respect the global animator duration scale setting</li>
|
||||
</ul>
|
||||
<p>Various other minor improvements</p>
|
||||
<h3>Version 1.1.4</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The export filename was missing the ".json" extension in some cases</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Password reminder for users who use biometric unlock</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Tokens would not refresh in some rare cases</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Ability to select multiple entries</li>
|
||||
<li>Ability to select a file location when exporting the vault (including cloud providers like Google Drive)</li>
|
||||
<li>Explanation and warning for the security options</li>
|
||||
<li>Removed external storage permissions</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Exporting the vault did not work on Android 10</li>
|
||||
</ul>
|
||||
<h3>Version 1.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for other types of biometric authentication (i.e. Pixel 4 face unlock)</li>
|
||||
<li>Support for importing from WinAuth</li>
|
||||
<li>Support for Chromebooks</li>
|
||||
<li>Option to highlight entries when tapped</li>
|
||||
<li>Filter for ungrouped tokens</li>
|
||||
<li>Ability to search for token account names</li>
|
||||
<li>Simplified Chinese translation (thanks RunningMelos!)</li>
|
||||
<li>Updated translations (thanks to all Crowdin contributers!)</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The behavior of highlighting and revealing entries was inconsistent</li>
|
||||
<li>The changelog dialog didn't work</li>
|
||||
<li>The persistent notification was shown even after the app was killed</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for andOTP's new backup file format</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.2</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Search feature on Huawei devices</li>
|
||||
</ul>
|
||||
<h4>Notes</h4>
|
||||
<ul>
|
||||
<li>Disabled automatic backups through the Google Play Store</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.1</h3>
|
||||
<h4>Notes</h4>
|
||||
<ul>
|
||||
<li>Temporarily disabled search feature on Huawei devices</li>
|
||||
</ul>
|
||||
<h3>Version 1.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>New icon</li>
|
||||
<li>Overhaul of interaction with the entry list</li>
|
||||
<li>Persistent notification while the vault is unlocked</li>
|
||||
<li>Language override option</li>
|
||||
<li>Support for importing from FreeOTP+</li>
|
||||
<li>Ability to toggle password visibility during unlock</li>
|
||||
<li>Support for deeplinking otpauth URIs</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Bad overall performance and high battery usage</li>
|
||||
<li>Codes with an uneven number of digits are displayed incorrectly</li>
|
||||
<li>Crash when entering a large value for OTP period</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: %2$s;
|
||||
color: %3$s;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre>%1$s</pre>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,42 +1,178 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class AegisApplication extends Application {
|
||||
private DatabaseManager _manager;
|
||||
private VaultFile _vaultFile;
|
||||
private VaultManager _manager;
|
||||
private Preferences _prefs;
|
||||
private List<LockListener> _lockListeners;
|
||||
private boolean _blockAutoLock;
|
||||
private IconPackManager _iconPackManager;
|
||||
|
||||
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
|
||||
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
|
||||
|
||||
static {
|
||||
// to access other app's internal storage directory, run libsu commands inside the global mount namespace
|
||||
Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_manager = new DatabaseManager(this);
|
||||
_prefs = new Preferences(this);
|
||||
_lockListeners = new ArrayList<>();
|
||||
_iconPackManager = new IconPackManager(this);
|
||||
|
||||
Iconics.init(this);
|
||||
Iconics.registerFont(new MaterialDesignIconic());
|
||||
|
||||
// listen for SCREEN_OFF events
|
||||
ScreenOffReceiver receiver = new ScreenOffReceiver();
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
intentFilter.addAction(CODE_LOCK_VAULT_ACTION);
|
||||
registerReceiver(receiver, intentFilter);
|
||||
|
||||
// lock the app if the user moves the application to the background
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
|
||||
|
||||
// clear the cache directory on startup, to make sure no temporary vault export files remain
|
||||
IOUtils.clearDirectory(getCacheDir(), false);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
initAppShortcuts();
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
initNotificationChannels();
|
||||
}
|
||||
}
|
||||
|
||||
public DatabaseManager getDatabaseManager() {
|
||||
public boolean isVaultLocked() {
|
||||
return _manager == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the vault file from disk at the default location, stores an internal
|
||||
* reference to it for future use and returns it. This must only be called before
|
||||
* initVaultManager() or after lock().
|
||||
*/
|
||||
public VaultFile loadVaultFile() throws VaultManagerException {
|
||||
if (!isVaultLocked()) {
|
||||
throw new AssertionError("loadVaultFile() may only be called before initVaultManager() or after lock()");
|
||||
}
|
||||
|
||||
if (_vaultFile == null) {
|
||||
_vaultFile = VaultManager.readVaultFile(this);
|
||||
}
|
||||
|
||||
return _vaultFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault manager by decrypting the given vaultFile with the given
|
||||
* creds. This removes the internal reference to the raw vault file.
|
||||
*/
|
||||
public VaultManager initVaultManager(VaultFile vaultFile, VaultFileCredentials creds) throws VaultManagerException {
|
||||
_vaultFile = null;
|
||||
_manager = VaultManager.init(this, vaultFile, creds);
|
||||
return _manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault manager with the given vault and creds. This removes the
|
||||
* internal reference to the raw vault file.
|
||||
*/
|
||||
public VaultManager initVaultManager(Vault vault, VaultFileCredentials creds) {
|
||||
_vaultFile = null;
|
||||
_manager = new VaultManager(this, vault, creds);
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public VaultManager getVaultManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public IconPackManager getIconPackManager() {
|
||||
return _iconPackManager;
|
||||
}
|
||||
|
||||
public Preferences getPreferences() {
|
||||
return _prefs;
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled(int autoLockType) {
|
||||
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
|
||||
}
|
||||
|
||||
public void registerLockListener(LockListener listener) {
|
||||
_lockListeners.add(listener);
|
||||
}
|
||||
|
||||
public void unregisterLockListener(LockListener listener) {
|
||||
_lockListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to block automatic lock on minimization. This should only be called
|
||||
* by activities before invoking an intent that shows a DocumentsUI, because that
|
||||
* action leads AppLifecycleObserver to believe that the app has been minimized.
|
||||
*/
|
||||
public void setBlockAutoLock(boolean block) {
|
||||
_blockAutoLock = block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the vault and the app.
|
||||
* @param userInitiated whether or not the user initiated the lock in MainActivity.
|
||||
*/
|
||||
public void lock(boolean userInitiated) {
|
||||
_manager.destroy();
|
||||
_manager = null;
|
||||
|
||||
for (LockListener listener : _lockListeners) {
|
||||
listener.onLocked(userInitiated);
|
||||
}
|
||||
|
||||
stopService(new Intent(AegisApplication.this, NotificationService.class));
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
|
||||
private void initAppShortcuts() {
|
||||
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
|
||||
@@ -46,15 +182,58 @@ public class AegisApplication extends Application {
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.putExtra("action", "scan");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
|
||||
.setShortLabel(getString(R.string.new_profile))
|
||||
.setLongLabel(getString(R.string.add_new_profile))
|
||||
.setIcon(Icon.createWithResource(this, R.drawable.qr_scanner))
|
||||
.setShortLabel(getString(R.string.new_entry))
|
||||
.setLongLabel(getString(R.string.add_new_entry))
|
||||
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
|
||||
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
|
||||
}
|
||||
|
||||
private void initNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = getString(R.string.channel_name_lock_status);
|
||||
String description = getString(R.string.channel_description_lock_status);
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CODE_LOCK_STATUS_ID, name, importance);
|
||||
channel.setDescription(description);
|
||||
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private class AppLifecycleObserver implements LifecycleEventObserver {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
|
||||
if (event == Lifecycle.Event.ON_STOP
|
||||
&& isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)
|
||||
&& !_blockAutoLock) {
|
||||
lock(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ScreenOffReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (isAutoLockEnabled(Preferences.AUTO_LOCK_ON_DEVICE_LOCK)) {
|
||||
lock(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface LockListener {
|
||||
/**
|
||||
* When called, the app/vault has been locked and the listener should perform its cleanup operations.
|
||||
* @param userInitiated whether or not the user initiated the lock in MainActivity.
|
||||
*/
|
||||
void onLocked(boolean userInitiated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.backup.BackupAgent;
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
import android.app.backup.FullBackupDataOutput;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AegisBackupAgent extends BackupAgent {
|
||||
private static final String TAG = BackupAgent.class.getSimpleName();
|
||||
|
||||
private Preferences _prefs;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_prefs = new Preferences(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onFullBackup(FullBackupDataOutput data) throws IOException {
|
||||
Log.i(TAG, String.format("onFullBackup() called: flags=%d, quota=%d",
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
|
||||
|
||||
if (!_prefs.isAndroidBackupsEnabled()) {
|
||||
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
|
||||
return;
|
||||
}
|
||||
|
||||
// first copy the vault to the files/backup directory
|
||||
createBackupDir();
|
||||
File vaultBackupFile = getVaultBackupFile();
|
||||
try (FileInputStream inStream = VaultManager.getAtomicFile(this).openRead();
|
||||
FileOutputStream outStream = new FileOutputStream(vaultBackupFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
|
||||
deleteBackupDir();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// then call the original implementation so that fullBackupContent specified in AndroidManifest is read
|
||||
try {
|
||||
super.onFullBackup(data);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
|
||||
throw e;
|
||||
} finally {
|
||||
deleteBackupDir();
|
||||
}
|
||||
|
||||
Log.i(TAG, "onFullBackup() finished");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
|
||||
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
|
||||
super.onRestoreFile(data, size, destination, type, mode, mtime);
|
||||
|
||||
File vaultBackupFile = getVaultBackupFile();
|
||||
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
|
||||
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
|
||||
VaultManager.writeToFile(this, inStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
|
||||
throw e;
|
||||
} finally {
|
||||
deleteBackupDir();
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format("onRestoreFile() finished: dest=%s", destination));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
|
||||
super.onQuotaExceeded(backupDataBytes, quotaBytes);
|
||||
Log.e(TAG, String.format("onQuotaExceeded() called: backupDataBytes=%d, quotaBytes=%d", backupDataBytes, quotaBytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
|
||||
|
||||
}
|
||||
|
||||
private void createBackupDir() throws IOException {
|
||||
File dir = getVaultBackupFile().getParentFile();
|
||||
if (!dir.exists() && !dir.mkdir()) {
|
||||
throw new IOException(String.format("Unable to create backup directory: %s", dir.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteBackupDir() {
|
||||
File dir = getVaultBackupFile().getParentFile();
|
||||
IOUtils.clearDirectory(dir, true);
|
||||
}
|
||||
|
||||
private File getVaultBackupFile() {
|
||||
return new File(new File(getFilesDir(), "backup"), VaultManager.FILENAME);
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,121 @@ package com.beemdevelopment.aegis;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Preferences {
|
||||
public static final int AUTO_LOCK_OFF = 1 << 0;
|
||||
public static final int AUTO_LOCK_ON_BACK_BUTTON = 1 << 1;
|
||||
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
|
||||
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
|
||||
|
||||
public static final int[] AUTO_LOCK_SETTINGS = {
|
||||
AUTO_LOCK_ON_BACK_BUTTON,
|
||||
AUTO_LOCK_ON_MINIMIZE,
|
||||
AUTO_LOCK_ON_DEVICE_LOCK
|
||||
};
|
||||
|
||||
private SharedPreferences _prefs;
|
||||
|
||||
public Preferences(Context context) {
|
||||
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
}
|
||||
|
||||
public boolean isDarkModeEnabled() {
|
||||
return _prefs.getBoolean("pref_dark_mode", false);
|
||||
if (getPasswordReminderTimestamp().getTime() == 0) {
|
||||
resetPasswordReminderTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTapToRevealEnabled() {
|
||||
return _prefs.getBoolean("pref_tap_to_reveal", false);
|
||||
}
|
||||
|
||||
public boolean isEntryHighlightEnabled() {
|
||||
return _prefs.getBoolean("pref_highlight_entry", false);
|
||||
}
|
||||
|
||||
public boolean isPanicTriggerEnabled() {
|
||||
return _prefs.getBoolean("pref_panic_trigger", false);
|
||||
}
|
||||
|
||||
public boolean isSecureScreenEnabled() {
|
||||
return _prefs.getBoolean("pref_secure_screen", true);
|
||||
// screen security should be enabled by default, but not for debug builds
|
||||
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
|
||||
}
|
||||
|
||||
public boolean isPasswordReminderEnabled() {
|
||||
return _prefs.getBoolean("pref_password_reminder", true);
|
||||
}
|
||||
|
||||
public boolean isPasswordReminderNeeded() {
|
||||
long diff = new Date().getTime() - getPasswordReminderTimestamp().getTime();
|
||||
long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
|
||||
return isPasswordReminderEnabled() && days >= 30;
|
||||
}
|
||||
|
||||
public Date getPasswordReminderTimestamp() {
|
||||
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
|
||||
}
|
||||
|
||||
public void resetPasswordReminderTimestamp() {
|
||||
_prefs.edit().putLong("pref_password_reminder_counter", new Date().getTime()).apply();
|
||||
}
|
||||
|
||||
public boolean isAccountNameVisible() {
|
||||
return _prefs.getBoolean("pref_account_name", false);
|
||||
return _prefs.getBoolean("pref_account_name", true);
|
||||
}
|
||||
|
||||
public int getCodeGroupSize() {
|
||||
if (_prefs.getBoolean("pref_code_group_size", false)) {
|
||||
return 2;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isIntroDone() {
|
||||
return _prefs.getBoolean("pref_intro", false);
|
||||
}
|
||||
|
||||
private int getAutoLockMask() {
|
||||
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
|
||||
if (!_prefs.contains("pref_auto_lock_mask")) {
|
||||
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
|
||||
}
|
||||
|
||||
return _prefs.getInt("pref_auto_lock_mask", def);
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return getAutoLockMask() != AUTO_LOCK_OFF;
|
||||
}
|
||||
|
||||
public boolean isAutoLockTypeEnabled(int autoLockType) {
|
||||
return (getAutoLockMask() & autoLockType) == autoLockType;
|
||||
}
|
||||
|
||||
public void setAutoLockMask(int autoLock) {
|
||||
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
|
||||
}
|
||||
|
||||
public void setIntroDone(boolean done) {
|
||||
_prefs.edit().putBoolean("pref_intro", done).apply();
|
||||
}
|
||||
@@ -39,12 +125,188 @@ public class Preferences {
|
||||
_prefs.edit().putInt("pref_tap_to_reveal_time", number).apply();
|
||||
}
|
||||
|
||||
public void setCurrentSortCategory(SortCategory category) {
|
||||
_prefs.edit().putInt("pref_current_sort_category", category.ordinal()).apply();
|
||||
}
|
||||
|
||||
public SortCategory getCurrentSortCategory() {
|
||||
return SortCategory.fromInteger(_prefs.getInt("pref_current_sort_category", 0));
|
||||
}
|
||||
|
||||
public int getTapToRevealTime() {
|
||||
return _prefs.getInt("pref_tap_to_reveal_time", 30);
|
||||
}
|
||||
|
||||
public Theme getCurrentTheme() {
|
||||
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
|
||||
}
|
||||
|
||||
public void setCurrentTheme(Theme theme) {
|
||||
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
|
||||
}
|
||||
|
||||
public ViewMode getCurrentViewMode() {
|
||||
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
|
||||
}
|
||||
|
||||
public void setCurrentViewMode(ViewMode viewMode) {
|
||||
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
|
||||
}
|
||||
|
||||
public Integer getUsageCount(UUID uuid) {
|
||||
Integer usageCount = getUsageCounts().get(uuid);
|
||||
|
||||
return usageCount != null ? usageCount : 0;
|
||||
}
|
||||
|
||||
public void resetUsageCount(UUID uuid) {
|
||||
Map<UUID, Integer> usageCounts = getUsageCounts();
|
||||
usageCounts.put(uuid, 0);
|
||||
|
||||
setUsageCount(usageCounts);
|
||||
}
|
||||
|
||||
public void clearUsageCount() {
|
||||
_prefs.edit().remove("pref_usage_count").apply();
|
||||
}
|
||||
|
||||
public Map<UUID, Integer> getUsageCounts() {
|
||||
Map<UUID, Integer> usageCounts = new HashMap<>();
|
||||
String usageCount = _prefs.getString("pref_usage_count", "");
|
||||
try {
|
||||
JSONArray arr = new JSONArray(usageCount);
|
||||
for(int i = 0; i < arr.length(); i++) {
|
||||
JSONObject json = arr.getJSONObject(i);
|
||||
usageCounts.put(UUID.fromString(json.getString("uuid")), json.getInt("count"));
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return usageCounts;
|
||||
}
|
||||
|
||||
public void setUsageCount(Map<UUID, Integer> usageCounts) {
|
||||
JSONArray usageCountJson = new JSONArray();
|
||||
for (Map.Entry<UUID, Integer> entry : usageCounts.entrySet()) {
|
||||
JSONObject entryJson = new JSONObject();
|
||||
try {
|
||||
entryJson.put("uuid", entry.getKey());
|
||||
entryJson.put("count", entry.getValue());
|
||||
usageCountJson.put(entryJson);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
_prefs.edit().putString("pref_usage_count", usageCountJson.toString()).apply();
|
||||
}
|
||||
|
||||
public int getTimeout() {
|
||||
return _prefs.getInt("pref_timeout", -1);
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
String lang = _prefs.getString("pref_lang", "system");
|
||||
|
||||
if (lang.equals("system")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return Resources.getSystem().getConfiguration().getLocales().get(0);
|
||||
} else {
|
||||
return Resources.getSystem().getConfiguration().locale;
|
||||
}
|
||||
}
|
||||
|
||||
String[] parts = lang.split("_");
|
||||
if (parts.length == 1) {
|
||||
return new Locale(parts[0]);
|
||||
}
|
||||
|
||||
return new Locale(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
public boolean isAndroidBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_android_backups", false);
|
||||
}
|
||||
|
||||
public void setIsAndroidBackupsEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
|
||||
}
|
||||
|
||||
public boolean isBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_backups", false);
|
||||
}
|
||||
|
||||
public void setIsBackupsEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_backups", enabled).apply();
|
||||
}
|
||||
|
||||
public Uri getBackupsLocation() {
|
||||
String str = _prefs.getString("pref_backups_location", null);
|
||||
if (str != null) {
|
||||
return Uri.parse(str);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setBackupsLocation(Uri location) {
|
||||
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
|
||||
}
|
||||
|
||||
public int getBackupsVersionCount() {
|
||||
return _prefs.getInt("pref_backups_versions", 5);
|
||||
}
|
||||
|
||||
public void setBackupsVersionCount(int versions) {
|
||||
_prefs.edit().putInt("pref_backups_versions", versions).apply();
|
||||
}
|
||||
|
||||
public void setBackupsError(Exception e) {
|
||||
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
|
||||
}
|
||||
|
||||
public String getBackupsError() {
|
||||
return _prefs.getString("pref_backups_error", null);
|
||||
}
|
||||
|
||||
public boolean isPinKeyboardEnabled() {
|
||||
return _prefs.getBoolean("pref_pin_keyboard", false);
|
||||
}
|
||||
|
||||
public boolean isTimeSyncWarningEnabled() {
|
||||
return _prefs.getBoolean("pref_warn_time_sync", true);
|
||||
}
|
||||
|
||||
public void setIsTimeSyncWarningEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
|
||||
}
|
||||
|
||||
public boolean isCopyOnTapEnabled() {
|
||||
return _prefs.getBoolean("pref_copy_on_tap", false);
|
||||
}
|
||||
|
||||
public void setGroupFilter(List<String> groupFilter) {
|
||||
JSONArray json = new JSONArray(groupFilter);
|
||||
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
|
||||
}
|
||||
|
||||
public List<String> getGroupFilter() {
|
||||
String raw = _prefs.getString("pref_group_filter", null);
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
JSONArray json = new JSONArray(raw);
|
||||
List<String> filter = new ArrayList<>();
|
||||
for (int i = 0; i < json.length(); i++) {
|
||||
filter.add(json.getString(i));
|
||||
}
|
||||
return filter;
|
||||
} catch (JSONException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
public enum SortCategory {
|
||||
CUSTOM,
|
||||
ACCOUNT,
|
||||
ACCOUNT_REVERSED,
|
||||
ISSUER,
|
||||
ISSUER_REVERSED,
|
||||
USAGE_COUNT;
|
||||
|
||||
private static SortCategory[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static SortCategory fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
|
||||
public Comparator<VaultEntry> getComparator() {
|
||||
Comparator<VaultEntry> comparator = null;
|
||||
|
||||
switch (this) {
|
||||
case ACCOUNT:
|
||||
comparator = new AccountNameComparator();
|
||||
break;
|
||||
case ACCOUNT_REVERSED:
|
||||
comparator = Collections.reverseOrder(new AccountNameComparator());
|
||||
break;
|
||||
case ISSUER:
|
||||
comparator = new IssuerNameComparator();
|
||||
break;
|
||||
case ISSUER_REVERSED:
|
||||
comparator = Collections.reverseOrder(new IssuerNameComparator());
|
||||
break;
|
||||
case USAGE_COUNT:
|
||||
comparator = Collections.reverseOrder(new UsageCountComparator());
|
||||
break;
|
||||
}
|
||||
|
||||
return comparator;
|
||||
}
|
||||
|
||||
public int getMenuItem() {
|
||||
switch (this) {
|
||||
case CUSTOM:
|
||||
return R.id.menu_sort_custom;
|
||||
case ACCOUNT:
|
||||
return R.id.menu_sort_alphabetically_name;
|
||||
case ACCOUNT_REVERSED:
|
||||
return R.id.menu_sort_alphabetically_name_reverse;
|
||||
case ISSUER:
|
||||
return R.id.menu_sort_alphabetically;
|
||||
case ISSUER_REVERSED:
|
||||
return R.id.menu_sort_alphabetically_reverse;
|
||||
case USAGE_COUNT:
|
||||
return R.id.menu_sort_usage_count;
|
||||
default:
|
||||
return R.id.menu_sort_custom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum Theme {
|
||||
LIGHT,
|
||||
DARK,
|
||||
AMOLED,
|
||||
SYSTEM,
|
||||
SYSTEM_AMOLED;
|
||||
|
||||
private static Theme[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static Theme fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ThemeMap {
|
||||
private ThemeMap() {
|
||||
|
||||
}
|
||||
|
||||
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Default,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Default,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Default
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_NoActionBar,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_NoActionBar,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_NoActionBar
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Fullscreen
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
|
||||
public enum ViewMode {
|
||||
NORMAL,
|
||||
COMPACT,
|
||||
SMALL;
|
||||
|
||||
private static ViewMode[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static ViewMode fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
public int getLayoutId() {
|
||||
switch (this) {
|
||||
case NORMAL:
|
||||
return R.layout.card_entry;
|
||||
case COMPACT:
|
||||
return R.layout.card_entry_compact;
|
||||
case SMALL:
|
||||
return R.layout.card_entry_small;
|
||||
default:
|
||||
return R.layout.card_entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
|
||||
*/
|
||||
public float getDividerHeight() {
|
||||
if (this == ViewMode.COMPACT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.encoding.HexException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -30,7 +30,7 @@ public class CryptParameters implements Serializable {
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException {
|
||||
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
|
||||
byte[] nonce = Hex.decode(obj.getString("nonce"));
|
||||
byte[] tag = Hex.decode(obj.getString("tag"));
|
||||
return new CryptParameters(nonce, tag);
|
||||
|
||||
@@ -2,11 +2,13 @@ package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -24,21 +26,24 @@ import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.spongycastle.crypto.generators.SCrypt;
|
||||
|
||||
public class CryptoUtils {
|
||||
public static final String CRYPTO_AEAD = "AES/GCM/NoPadding";
|
||||
public static final byte CRYPTO_AEAD_KEY_SIZE = 32;
|
||||
public static final byte CRYPTO_AEAD_TAG_SIZE = 16;
|
||||
public static final byte CRYPTO_AEAD_NONCE_SIZE = 12;
|
||||
|
||||
public static final int CRYPTO_SCRYPT_N = 1 << 15;
|
||||
public static final int CRYPTO_SCRYPT_r = 8;
|
||||
public static final int CRYPTO_SCRYPT_p = 1;
|
||||
|
||||
public static SecretKey deriveKey(byte[] input, SCryptParameters params) {
|
||||
byte[] keyBytes = SCrypt.generate(input, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
|
||||
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
|
||||
}
|
||||
|
||||
public static SecretKey deriveKey(char[] password, SCryptParameters params) {
|
||||
byte[] bytes = toBytes(password);
|
||||
byte[] keyBytes = SCrypt.generate(bytes, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
|
||||
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
|
||||
return deriveKey(bytes, params);
|
||||
}
|
||||
|
||||
public static Cipher createEncryptCipher(SecretKey key)
|
||||
@@ -88,9 +93,14 @@ public class CryptoUtils {
|
||||
|
||||
public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params)
|
||||
throws IOException, BadPaddingException, IllegalBlockSizeException {
|
||||
return decrypt(encrypted, 0, encrypted.length, cipher, params);
|
||||
}
|
||||
|
||||
public static CryptResult decrypt(byte[] encrypted, int encryptedOffset, int encryptedLen, Cipher cipher, CryptParameters params)
|
||||
throws IOException, BadPaddingException, IllegalBlockSizeException {
|
||||
// append the tag to the ciphertext
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(encrypted);
|
||||
stream.write(encrypted, encryptedOffset, encryptedLen);
|
||||
stream.write(params.getTag());
|
||||
|
||||
encrypted = stream.toByteArray();
|
||||
@@ -120,9 +130,18 @@ public class CryptoUtils {
|
||||
return data;
|
||||
}
|
||||
|
||||
private static byte[] toBytes(char[] chars) {
|
||||
public static byte[] toBytes(char[] chars) {
|
||||
CharBuffer charBuf = CharBuffer.wrap(chars);
|
||||
ByteBuffer byteBuf = Charset.forName("UTF-8").encode(charBuf);
|
||||
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
|
||||
byte[] bytes = new byte[byteBuf.limit()];
|
||||
byteBuf.get(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static byte[] toBytesOld(char[] chars) {
|
||||
CharBuffer charBuf = CharBuffer.wrap(chars);
|
||||
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
|
||||
return byteBuf.array();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -12,8 +13,10 @@ import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.ProviderException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
@@ -58,6 +61,14 @@ public class KeyStoreHandle {
|
||||
.build());
|
||||
|
||||
return generator.generateKey();
|
||||
} catch (ProviderException e) {
|
||||
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
|
||||
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
|
||||
throw new KeyStoreHandleException(cause);
|
||||
}
|
||||
throw e;
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
@@ -76,22 +87,29 @@ public class KeyStoreHandle {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
|
||||
// try to initialize a dummy cipher
|
||||
// and see if KeyPermanentlyInvalidatedException is thrown
|
||||
if (isSupported()) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
} catch (KeyPermanentlyInvalidatedException e) {
|
||||
return null;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (isSupported() && isKeyPermanentlyInvalidated(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
|
||||
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
} catch (InvalidKeyException e) {
|
||||
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void deleteKey(String id) throws KeyStoreHandleException {
|
||||
try {
|
||||
_keyStore.deleteEntry(id);
|
||||
@@ -100,6 +118,16 @@ public class KeyStoreHandle {
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() throws KeyStoreHandleException {
|
||||
try {
|
||||
for (String alias : Collections.list(_keyStore.aliases())) {
|
||||
deleteKey(alias);
|
||||
}
|
||||
} catch (KeyStoreException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public class MasterKey implements Serializable {
|
||||
|
||||
public MasterKey(SecretKey key) {
|
||||
if (key == null) {
|
||||
throw new NullPointerException();
|
||||
throw new IllegalArgumentException("Key cannot be null");
|
||||
}
|
||||
_key = key;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.beemdevelopment.aegis.crypto.otp;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
@@ -12,7 +12,7 @@ public class HOTP {
|
||||
private HOTP() {
|
||||
}
|
||||
|
||||
public static String generateOTP(byte[] secret, String algo, int digits, long counter)
|
||||
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
|
||||
|
||||
@@ -30,18 +30,11 @@ public class HOTP {
|
||||
// truncate hash to get the HTOP value
|
||||
// http://tools.ietf.org/html/rfc4226#section-5.4
|
||||
int offset = hash[hash.length - 1] & 0xf;
|
||||
long bin = ((hash[offset] & 0x7f) << 24)
|
||||
int otp = ((hash[offset] & 0x7f) << 24)
|
||||
| ((hash[offset + 1] & 0xff) << 16)
|
||||
| ((hash[offset + 2] & 0xff) << 8)
|
||||
| (hash[offset + 3] & 0xff);
|
||||
long otp = bin % (long) Math.pow(10, digits);
|
||||
|
||||
// prepend zeroes if needed
|
||||
StringBuilder res = new StringBuilder(Long.toString(otp));
|
||||
while (res.length() < digits) {
|
||||
res.insert(0, "0");
|
||||
}
|
||||
|
||||
return res.toString();
|
||||
return new OTP(otp, digits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.beemdevelopment.aegis.crypto.otp;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class OTP {
|
||||
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
|
||||
|
||||
private int _code;
|
||||
private int _digits;
|
||||
|
||||
public OTP(int code, int digits) {
|
||||
_code = code;
|
||||
_digits = digits;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return _code;
|
||||
}
|
||||
|
||||
public int getDigits() {
|
||||
return _digits;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
int code = _code % (int) Math.pow(10, _digits);
|
||||
|
||||
// prepend zeroes if needed
|
||||
StringBuilder res = new StringBuilder(Long.toString(code));
|
||||
while (res.length() < _digits) {
|
||||
res.insert(0, "0");
|
||||
}
|
||||
|
||||
return res.toString();
|
||||
}
|
||||
|
||||
public String toSteamString() {
|
||||
int code = _code;
|
||||
StringBuilder res = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < _digits; i++) {
|
||||
char c = STEAM_ALPHABET.charAt(code % STEAM_ALPHABET.length());
|
||||
res.append(c);
|
||||
code /= STEAM_ALPHABET.length();
|
||||
}
|
||||
|
||||
return res.toString();
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@ public class TOTP {
|
||||
private TOTP() {
|
||||
}
|
||||
|
||||
public static String generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
|
||||
public static OTP generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
long counter = (long) Math.floor((double) seconds / period);
|
||||
return HOTP.generateOTP(secret, algo, digits, counter);
|
||||
}
|
||||
|
||||
public static String generateOTP(byte[] secret, String algo, int digits, long period)
|
||||
public static OTP generateOTP(byte[] secret, String algo, int digits, long period)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.crypto.pins;
|
||||
|
||||
import info.guardianproject.trustedintents.ApkSignaturePin;
|
||||
|
||||
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
|
||||
|
||||
public GuardianProjectFDroidRSA2048() {
|
||||
fingerprints = new String[]{
|
||||
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
|
||||
};
|
||||
certificates = new byte[][]{
|
||||
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.crypto.pins;
|
||||
|
||||
import info.guardianproject.trustedintents.ApkSignaturePin;
|
||||
|
||||
public final class GuardianProjectFDroidRSA4096 extends ApkSignaturePin {
|
||||
|
||||
public GuardianProjectFDroidRSA4096() {
|
||||
fingerprints = new String[]{
|
||||
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
|
||||
};
|
||||
certificates = new byte[][]{
|
||||
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Database {
|
||||
private static final int VERSION = 1;
|
||||
private DatabaseEntryList _entries = new DatabaseEntryList();
|
||||
|
||||
public JSONObject toJson() {
|
||||
try {
|
||||
JSONArray array = new JSONArray();
|
||||
for (DatabaseEntry e : _entries) {
|
||||
array.put(e.toJson());
|
||||
}
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("version", VERSION);
|
||||
obj.put("entries", array);
|
||||
return obj;
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Database fromJson(JSONObject obj) throws DatabaseException {
|
||||
Database db = new Database();
|
||||
|
||||
try {
|
||||
int ver = obj.getInt("version");
|
||||
if (ver != VERSION) {
|
||||
throw new DatabaseException("Unsupported version");
|
||||
}
|
||||
|
||||
JSONArray array = obj.getJSONArray("entries");
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i));
|
||||
db.addEntry(entry);
|
||||
}
|
||||
} catch (Base64Exception | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseException(e);
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
_entries.remove(entry);
|
||||
}
|
||||
|
||||
public void replaceEntry(DatabaseEntry newEntry) {
|
||||
_entries.replace(newEntry);
|
||||
}
|
||||
|
||||
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
_entries.swap(entry1, entry2);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
return _entries.getList();
|
||||
}
|
||||
|
||||
public DatabaseEntry getEntryByUUID(UUID uuid) {
|
||||
return _entries.getByUUID(uuid);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DatabaseEntry implements Serializable {
|
||||
private UUID _uuid;
|
||||
private String _name = "";
|
||||
private String _issuer = "";
|
||||
private String _group;
|
||||
private OtpInfo _info;
|
||||
private byte[] _icon;
|
||||
|
||||
private DatabaseEntry(UUID uuid, OtpInfo info) {
|
||||
_uuid = uuid;
|
||||
_info = info;
|
||||
}
|
||||
|
||||
public DatabaseEntry(OtpInfo info) {
|
||||
this(UUID.randomUUID(), info);
|
||||
}
|
||||
|
||||
public DatabaseEntry(OtpInfo info, String name, String issuer) {
|
||||
this(info);
|
||||
setName(name);
|
||||
setIssuer(issuer);
|
||||
}
|
||||
|
||||
public DatabaseEntry(GoogleAuthInfo info) {
|
||||
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
|
||||
}
|
||||
|
||||
public JSONObject toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("type", _info.getType());
|
||||
obj.put("uuid", _uuid.toString());
|
||||
obj.put("name", _name);
|
||||
obj.put("issuer", _issuer);
|
||||
obj.put("group", _group);
|
||||
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
|
||||
obj.put("info", _info.toJson());
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static DatabaseEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
|
||||
// if there is no uuid, generate a new one
|
||||
UUID uuid;
|
||||
if (!obj.has("uuid")) {
|
||||
uuid = UUID.randomUUID();
|
||||
} else {
|
||||
uuid = UUID.fromString(obj.getString("uuid"));
|
||||
}
|
||||
|
||||
OtpInfo info = OtpInfo.fromJson(obj.getString("type"), obj.getJSONObject("info"));
|
||||
DatabaseEntry entry = new DatabaseEntry(uuid, info);
|
||||
entry.setName(obj.getString("name"));
|
||||
entry.setIssuer(obj.getString("issuer"));
|
||||
entry.setGroup(obj.optString("group", null));
|
||||
|
||||
Object icon = obj.get("icon");
|
||||
if (icon != JSONObject.NULL) {
|
||||
entry.setIcon(Base64.decode((String) icon));
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
public void resetUUID() {
|
||||
_uuid = UUID.randomUUID();
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return _group;
|
||||
}
|
||||
|
||||
public byte[] getIcon() {
|
||||
return _icon;
|
||||
}
|
||||
|
||||
public OtpInfo getInfo() {
|
||||
return _info;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
_issuer = issuer;
|
||||
}
|
||||
|
||||
public void setGroup(String group) {
|
||||
_group = group;
|
||||
}
|
||||
|
||||
public void setInfo(OtpInfo info) {
|
||||
_info = info;
|
||||
}
|
||||
|
||||
public void setIcon(byte[] icon) {
|
||||
_icon = icon;
|
||||
}
|
||||
|
||||
public boolean hasIcon() {
|
||||
return _icon != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof DatabaseEntry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DatabaseEntry entry = (DatabaseEntry) o;
|
||||
return getUUID().equals(entry.getUUID())
|
||||
&& getName().equals(entry.getName())
|
||||
&& getIssuer().equals(entry.getIssuer())
|
||||
&& Objects.equals(getGroup(), entry.getGroup())
|
||||
&& getInfo().equals(entry.getInfo())
|
||||
&& Arrays.equals(getIcon(), entry.getIcon());
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DatabaseEntryList implements Iterable<DatabaseEntry>, Serializable {
|
||||
private List<DatabaseEntry> _entries = new ArrayList<>();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<DatabaseEntry> iterator() {
|
||||
return _entries.iterator();
|
||||
}
|
||||
|
||||
public void add(DatabaseEntry entry) {
|
||||
if (getByUUID(entry.getUUID()) != null) {
|
||||
throw new AssertionError("entry found with the same uuid");
|
||||
}
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void remove(DatabaseEntry entry) {
|
||||
entry = mustGetByUUID(entry.getUUID());
|
||||
_entries.remove(entry);
|
||||
}
|
||||
|
||||
public void replace(DatabaseEntry newEntry) {
|
||||
DatabaseEntry oldEntry = mustGetByUUID(newEntry.getUUID());
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
}
|
||||
|
||||
public void swap(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getList() {
|
||||
return Collections.unmodifiableList(_entries);
|
||||
}
|
||||
|
||||
public DatabaseEntry getByUUID(UUID uuid) {
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DatabaseEntry mustGetByUUID(UUID uuid) {
|
||||
DatabaseEntry entry = getByUUID(uuid);
|
||||
if (entry == null) {
|
||||
throw new AssertionError("no entry found with the same uuid");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
public class DatabaseException extends Exception {
|
||||
public DatabaseException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public DatabaseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
public class DatabaseFileException extends Exception {
|
||||
public DatabaseFileException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public DatabaseFileException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.Collator;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class DatabaseManager {
|
||||
private static final String FILENAME = "aegis.json";
|
||||
private static final String FILENAME_EXPORT = "aegis_export.json";
|
||||
private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
|
||||
|
||||
private Database _db;
|
||||
private DatabaseFile _file;
|
||||
private DatabaseFileCredentials _creds;
|
||||
private boolean _encrypt;
|
||||
|
||||
private Context _context;
|
||||
|
||||
public DatabaseManager(Context context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public boolean fileExists() {
|
||||
File file = new File(_context.getFilesDir(), FILENAME);
|
||||
return file.exists() && file.isFile();
|
||||
}
|
||||
|
||||
public void load() throws DatabaseManagerException {
|
||||
assertState(true, false);
|
||||
|
||||
try (FileInputStream file = _context.openFileInput(FILENAME)) {
|
||||
byte[] fileBytes = new byte[(int) file.getChannel().size()];
|
||||
DataInputStream stream = new DataInputStream(file);
|
||||
stream.readFully(fileBytes);
|
||||
stream.close();
|
||||
|
||||
_file = DatabaseFile.fromBytes(fileBytes);
|
||||
_encrypt = _file.isEncrypted();
|
||||
if (!isEncryptionEnabled()) {
|
||||
JSONObject obj = _file.getContent();
|
||||
_db = Database.fromJson(obj);
|
||||
}
|
||||
} catch (IOException | DatabaseFileException | DatabaseException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
assertState(false, true);
|
||||
_creds = null;
|
||||
_db = null;
|
||||
}
|
||||
|
||||
public void unlock(DatabaseFileCredentials creds) throws DatabaseManagerException {
|
||||
assertState(true, true);
|
||||
|
||||
try {
|
||||
JSONObject obj = _file.getContent(creds);
|
||||
_db = Database.fromJson(obj);
|
||||
_creds = creds;
|
||||
} catch (DatabaseFileException | DatabaseException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void save(Context context, DatabaseFile file) throws DatabaseManagerException {
|
||||
byte[] bytes = file.toBytes();
|
||||
try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
|
||||
stream.write(bytes);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() throws DatabaseManagerException {
|
||||
assertState(false, true);
|
||||
|
||||
try {
|
||||
JSONObject obj = _db.toJson();
|
||||
if (isEncryptionEnabled()) {
|
||||
_file.setContent(obj, _creds);
|
||||
} else {
|
||||
_file.setContent(obj);
|
||||
}
|
||||
save(_context, _file);
|
||||
} catch (DatabaseFileException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String export(boolean encrypt) throws DatabaseManagerException {
|
||||
assertState(false, true);
|
||||
|
||||
try {
|
||||
DatabaseFile dbFile = new DatabaseFile();
|
||||
if (encrypt && isEncryptionEnabled()) {
|
||||
dbFile.setContent(_db.toJson(), _creds);
|
||||
} else {
|
||||
dbFile.setContent(_db.toJson());
|
||||
}
|
||||
|
||||
String dirName = !BuildConfig.DEBUG ? _context.getString(R.string.app_name) : _context.getString(R.string.app_name_dev);
|
||||
File dir = new File(Environment.getExternalStorageDirectory(), dirName);
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
throw new IOException("error creating external storage directory");
|
||||
}
|
||||
|
||||
byte[] bytes = dbFile.toBytes();
|
||||
File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
|
||||
try (FileOutputStream stream = new FileOutputStream(file)) {
|
||||
stream.write(bytes);
|
||||
}
|
||||
|
||||
return file.getAbsolutePath();
|
||||
} catch (IOException | DatabaseFileException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.addEntry(entry);
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.removeEntry(entry);
|
||||
}
|
||||
|
||||
public void replaceEntry(DatabaseEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.replaceEntry(entry);
|
||||
}
|
||||
|
||||
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
assertState(false, true);
|
||||
_db.swapEntries(entry1, entry2);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
assertState(false, true);
|
||||
return _db.getEntries();
|
||||
}
|
||||
|
||||
public DatabaseEntry getEntryByUUID(UUID uuid) {
|
||||
assertState(false, true);
|
||||
return _db.getEntryByUUID(uuid);
|
||||
}
|
||||
|
||||
public TreeSet<String> getGroups() {
|
||||
assertState(false, true);
|
||||
|
||||
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
|
||||
for (DatabaseEntry entry : getEntries()) {
|
||||
String group = entry.getGroup();
|
||||
if (group != null) {
|
||||
groups.add(group);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
public DatabaseFileCredentials getCredentials() {
|
||||
assertState(false, true);
|
||||
return _creds;
|
||||
}
|
||||
|
||||
public void setCredentials(DatabaseFileCredentials creds) {
|
||||
assertState(false, true);
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public DatabaseFile.Header getFileHeader() {
|
||||
assertLoaded(true);
|
||||
return _file.getHeader();
|
||||
}
|
||||
|
||||
public boolean isEncryptionEnabled() {
|
||||
assertLoaded(true);
|
||||
return _encrypt;
|
||||
}
|
||||
|
||||
public void enableEncryption(DatabaseFileCredentials creds) throws DatabaseManagerException {
|
||||
assertState(false, true);
|
||||
_creds = creds;
|
||||
_encrypt = true;
|
||||
save();
|
||||
}
|
||||
|
||||
public void disableEncryption() throws DatabaseManagerException {
|
||||
assertState(false, true);
|
||||
_creds = null;
|
||||
_encrypt = false;
|
||||
save();
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return _file != null;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return _db == null;
|
||||
}
|
||||
|
||||
private void assertState(boolean locked, boolean loaded) {
|
||||
assertLoaded(loaded);
|
||||
|
||||
if (isLocked() && !locked) {
|
||||
throw new AssertionError("database file has not been unlocked yet");
|
||||
} else if (!isLocked() && locked) {
|
||||
throw new AssertionError("database file has already been unlocked");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertLoaded(boolean loaded) {
|
||||
if (isLoaded() && !loaded) {
|
||||
throw new AssertionError("database file has already been loaded");
|
||||
} else if (!isLoaded() && loaded) {
|
||||
throw new AssertionError("database file has not been loaded yet");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
public class DatabaseManagerException extends Exception {
|
||||
public DatabaseManagerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db.slots;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class FingerprintSlot extends RawSlot {
|
||||
public FingerprintSlot() {
|
||||
super();
|
||||
}
|
||||
|
||||
FingerprintSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
|
||||
super(uuid, key, keyParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getType() {
|
||||
return TYPE_FINGERPRINT;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +1,23 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
// modified for use in Aegis
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
/* (PD) 2001 The Bitzi Corporation
|
||||
* Please see http://bitzi.com/publicdomain for more info.
|
||||
*
|
||||
* As modified by Patrick Woodworth:
|
||||
*
|
||||
* Copyright 2011 Patrick Woodworth
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import java.util.Locale;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Base32 - encodes and decodes RFC3548 Base32
|
||||
* (see http://www.faqs.org/rfcs/rfc3548.html )
|
||||
*
|
||||
* @author Robert Kaye
|
||||
* @author Gordon Mohr
|
||||
*/
|
||||
public class Base32 {
|
||||
private static final String base32Chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
private static final int[] base32Lookup =
|
||||
{ 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
|
||||
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
|
||||
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
|
||||
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
|
||||
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
|
||||
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
|
||||
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
|
||||
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
|
||||
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
|
||||
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
|
||||
};
|
||||
private Base32() {
|
||||
|
||||
/**
|
||||
* Encodes byte array to Base32 String.
|
||||
*
|
||||
* @param bytes Bytes to encode.
|
||||
* @return Encoded byte array <code>bytes</code> as a String.
|
||||
*
|
||||
*/
|
||||
public static char[] encode(final byte[] bytes) {
|
||||
int i = 0, index = 0, digit = 0, j = 0;
|
||||
int currByte, nextByte;
|
||||
char[] base32 = new char[(bytes.length + 7) * 8 / 5];
|
||||
|
||||
while (i < bytes.length) {
|
||||
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
|
||||
|
||||
/* Is the current digit going to span a byte boundary? */
|
||||
if (index > 3) {
|
||||
if ((i + 1) < bytes.length) {
|
||||
nextByte =
|
||||
(bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256);
|
||||
} else {
|
||||
nextByte = 0;
|
||||
}
|
||||
|
||||
digit = currByte & (0xFF >> index);
|
||||
index = (index + 5) % 8;
|
||||
digit <<= index;
|
||||
digit |= nextByte >> (8 - index);
|
||||
i++;
|
||||
} else {
|
||||
digit = (currByte >> (8 - (index + 5))) & 0x1F;
|
||||
index = (index + 5) % 8;
|
||||
if (index == 0)
|
||||
i++;
|
||||
}
|
||||
base32[j++] = base32Chars.charAt(digit);
|
||||
}
|
||||
|
||||
return Arrays.copyOf(base32, j);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given Base32 String to a raw byte array.
|
||||
*
|
||||
* @param base32
|
||||
* @return Decoded <code>base32</code> String as a raw byte array.
|
||||
*/
|
||||
public static byte[] decode(final char[] base32) throws Base32Exception {
|
||||
int i, index, lookup, offset, digit;
|
||||
byte[] bytes = new byte[base32.length * 5 / 8];
|
||||
|
||||
for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
|
||||
// stop decoding when a padding char is encountered
|
||||
if (base32[i] == '=') {
|
||||
// make sure the rest is also padding, but don't bother verifying the length
|
||||
for (int j = i + 1; j < base32.length; j++) {
|
||||
if (base32[j] != '=') {
|
||||
throw new Base32Exception("bad padding");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
lookup = base32[i] - '0';
|
||||
digit = decodeDigit(lookup);
|
||||
|
||||
if (index <= 3) {
|
||||
index = (index + 5) % 8;
|
||||
if (index == 0) {
|
||||
bytes[offset] |= digit;
|
||||
offset++;
|
||||
if (offset >= bytes.length)
|
||||
break;
|
||||
} else {
|
||||
bytes[offset] |= digit << (8 - index);
|
||||
}
|
||||
} else {
|
||||
index = (index + 5) % 8;
|
||||
bytes[offset] |= (digit >>> index);
|
||||
offset++;
|
||||
|
||||
if (offset >= bytes.length) {
|
||||
break;
|
||||
}
|
||||
bytes[offset] |= digit << (8 - index);
|
||||
}
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base32().decode(s.toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static int decodeDigit(int c) throws Base32Exception {
|
||||
/* Skip chars outside the lookup table */
|
||||
if (c < 0 || c >= base32Lookup.length) {
|
||||
throw new Base32Exception("char not found in base32 lookup table");
|
||||
}
|
||||
|
||||
int digit = base32Lookup[c];
|
||||
|
||||
/* If this digit is not in the table, ignore it */
|
||||
if (digit == 0xFF) {
|
||||
throw new Base32Exception("char not found in base32 lookup table");
|
||||
}
|
||||
|
||||
return digit;
|
||||
public static String encode(byte[] data) {
|
||||
return BaseEncoding.base32().omitPadding().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class Base32Exception extends Exception {
|
||||
public Base32Exception(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,27 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class Base64 {
|
||||
private static final int _flags = android.util.Base64.NO_WRAP;
|
||||
|
||||
private Base64() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) throws Base64Exception {
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return android.util.Base64.decode(s, _flags);
|
||||
return BaseEncoding.base64().decode(s);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new Base64Exception(e);
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[] s) throws EncodingException {
|
||||
return decode(new String(s, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
byte[] encoded = android.util.Base64.encode(data, _flags);
|
||||
|
||||
try {
|
||||
return new String(encoded, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return BaseEncoding.base64().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class Base64Exception extends Exception {
|
||||
public Base64Exception(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class EncodingException extends IOException {
|
||||
public EncodingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,23 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
|
||||
// It is licensed under GPLv2 with a classpath exception.
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class Hex {
|
||||
private Hex() {
|
||||
|
||||
}
|
||||
|
||||
private static int hexToBin(char ch) {
|
||||
if ('0' <= ch && ch <= '9') return ch - '0';
|
||||
if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
|
||||
if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static final char[] hexCode = "0123456789abcdef".toCharArray();
|
||||
|
||||
public static byte[] decode(String s) throws HexException {
|
||||
final int len = s.length();
|
||||
|
||||
if (len % 2 != 0)
|
||||
throw new HexException("hexBinary needs to be even-length: " + s);
|
||||
|
||||
byte[] out = new byte[len / 2];
|
||||
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
int h = hexToBin(s.charAt(i));
|
||||
int l = hexToBin(s.charAt(i + 1));
|
||||
if (h == -1 || l == -1)
|
||||
throw new HexException("contains illegal character for hexBinary: " + s);
|
||||
|
||||
out[i / 2] = (byte) (h * 16 + l);
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base16().decode(s.toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
StringBuilder r = new StringBuilder(data.length * 2);
|
||||
for (byte b : data) {
|
||||
r.append(hexCode[(b >> 4) & 0xF]);
|
||||
r.append(hexCode[(b & 0xF)]);
|
||||
}
|
||||
return r.toString();
|
||||
return BaseEncoding.base16().lowerCase().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
public class HexException extends Exception {
|
||||
public HexException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
/**
|
||||
* A class that can prepare initialization of a BiometricSlot by generating a new
|
||||
* key in the Android KeyStore and authenticating a cipher for it through a
|
||||
* BiometricPrompt.
|
||||
*/
|
||||
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
|
||||
private BiometricSlot _slot;
|
||||
private Listener _listener;
|
||||
private BiometricPrompt _prompt;
|
||||
|
||||
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
|
||||
_listener = listener;
|
||||
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
|
||||
}
|
||||
|
||||
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
|
||||
_listener = listener;
|
||||
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new key in the Android KeyStore for the new BiometricSlot,
|
||||
* initializes a cipher with it and shows a BiometricPrompt to the user for
|
||||
* authentication. If authentication is successful, the new slot will be
|
||||
* initialized and delivered back through the listener.
|
||||
*/
|
||||
public void authenticate(BiometricPrompt.PromptInfo info) {
|
||||
if (_slot != null) {
|
||||
throw new IllegalStateException("Biometric authentication already in progress");
|
||||
}
|
||||
|
||||
KeyStoreHandle keyStore;
|
||||
try {
|
||||
keyStore = new KeyStoreHandle();
|
||||
} catch (KeyStoreHandleException e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// generate a new Android KeyStore key
|
||||
// and assign it the UUID of the new slot as an alias
|
||||
Cipher cipher;
|
||||
BiometricSlot slot = new BiometricSlot();
|
||||
try {
|
||||
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
|
||||
cipher = Slot.createEncryptCipher(key);
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
|
||||
_slot = slot;
|
||||
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the BiometricPrompt and resets the state of the initializer. It will
|
||||
* also attempt to delete the previously generated Android KeyStore key.
|
||||
*/
|
||||
public void cancelAuthentication() {
|
||||
if (_slot == null) {
|
||||
throw new IllegalStateException("Biometric authentication not in progress");
|
||||
}
|
||||
|
||||
reset();
|
||||
_prompt.cancelAuthentication();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
if (_slot != null) {
|
||||
try {
|
||||
// clean up the unused KeyStore key
|
||||
// this is non-critical, so just fail silently if an error occurs
|
||||
String uuid = _slot.getUUID().toString();
|
||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||
if (keyStore.containsKey(uuid)) {
|
||||
keyStore.deleteKey(uuid);
|
||||
}
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
_slot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(int errorCode, CharSequence errString) {
|
||||
reset();
|
||||
_listener.onSlotInitializationFailed(errorCode, errString);
|
||||
}
|
||||
|
||||
private void fail(Exception e) {
|
||||
e.printStackTrace();
|
||||
fail(0, e.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
fail(errorCode, errString.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
|
||||
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
public class BiometricsHelper {
|
||||
private BiometricsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static BiometricManager getManager(Context context) {
|
||||
BiometricManager manager = BiometricManager.from(context);
|
||||
if (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
return manager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isCanceled(int errorCode) {
|
||||
return errorCode == BiometricPrompt.ERROR_CANCELED
|
||||
|| errorCode == BiometricPrompt.ERROR_USER_CANCELED
|
||||
|| errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON;
|
||||
}
|
||||
|
||||
public static boolean isAvailable(Context context) {
|
||||
return getManager(context) != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
public class BitmapHelper {
|
||||
private BitmapHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the given Bitmap to the given maximum width/height, while keeping the aspect ratio intact.
|
||||
*/
|
||||
public static Bitmap resize(Bitmap bitmap, int maxWidth, int maxHeight) {
|
||||
if (maxHeight <= 0 || maxWidth <= 0) {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
float maxRatio = (float) maxWidth / maxHeight;
|
||||
float ratio = (float) bitmap.getWidth() / bitmap.getHeight();
|
||||
|
||||
int width = maxWidth;
|
||||
int height = maxHeight;
|
||||
if (maxRatio > 1) {
|
||||
width = (int) ((float) maxHeight * ratio);
|
||||
} else {
|
||||
height = (int) ((float) maxWidth / ratio);
|
||||
}
|
||||
|
||||
return Bitmap.createScaledBitmap(bitmap, width, height, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* ContextHelper contains some disgusting hacks to obtain the Activity/Lifecycle from a Context.
|
||||
*/
|
||||
public class ContextHelper {
|
||||
private ContextHelper() {
|
||||
|
||||
}
|
||||
|
||||
// source: https://github.com/androidx/androidx/blob/e32e1da51a0c7448c74861c667fa76738a415a89/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java#L425-L435
|
||||
@Nullable
|
||||
public static ComponentActivity getActivity(@NonNull Context context) {
|
||||
while (context instanceof ContextWrapper) {
|
||||
if (context instanceof ComponentActivity) {
|
||||
return (ComponentActivity) context;
|
||||
}
|
||||
|
||||
context = ((ContextWrapper) context).getBaseContext();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Lifecycle getLifecycle(@NonNull Context context) {
|
||||
ComponentActivity activity = getActivity(context);
|
||||
return activity == null ? null : activity.getLifecycle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DropdownHelper {
|
||||
private DropdownHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, @ArrayRes int textArrayResId) {
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, R.layout.dropdown_list_item);
|
||||
dropdown.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public static <T> void fillDropdown(Context context, AutoCompleteTextView dropdown, List<T> items) {
|
||||
ArrayAdapter<T> adapter = new ArrayAdapter<>(context, R.layout.dropdown_list_item, items);
|
||||
dropdown.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,6 @@ public class EditTextHelper {
|
||||
private EditTextHelper() {
|
||||
}
|
||||
|
||||
public static void clearEditText(EditText text) {
|
||||
text.getText().clear();
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text) {
|
||||
Editable editable = text.getText();
|
||||
char[] chars = new char[editable.length()];
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.view.View;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
public class FabScrollHelper {
|
||||
private View _fabMenu;
|
||||
private boolean _isAnimating;
|
||||
|
||||
public FabScrollHelper(View floatingActionsMenu) {
|
||||
_fabMenu = floatingActionsMenu;
|
||||
}
|
||||
|
||||
public void onScroll(int dx, int dy) {
|
||||
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
|
||||
setVisible(false);
|
||||
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
|
||||
setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
if (visible) {
|
||||
_fabMenu.setVisibility(View.VISIBLE);
|
||||
_fabMenu.animate()
|
||||
.translationY(0)
|
||||
.setInterpolator(new DecelerateInterpolator(2))
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
_isAnimating = false;
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
_isAnimating = true;
|
||||
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
|
||||
int fabBottomMargin = lp.bottomMargin;
|
||||
_fabMenu.animate()
|
||||
.translationY(_fabMenu.getHeight() + fabBottomMargin)
|
||||
.setInterpolator(new AccelerateInterpolator(2))
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
_isAnimating = false;
|
||||
_fabMenu.setVisibility(View.INVISIBLE);
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
|
||||
public class FingerprintHelper {
|
||||
private FingerprintHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static FingerprintManager getManager(Context context) {
|
||||
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
|
||||
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
|
||||
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java
|
||||
// It has been modified to suit Aegis' needs
|
||||
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mattprecious.swirl.SwirlView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
/**
|
||||
* Small helper class to manage text/icon around fingerprint authentication UI.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
|
||||
|
||||
private static final long ERROR_TIMEOUT_MILLIS = 1600;
|
||||
private static final long SUCCESS_DELAY_MILLIS = 100;
|
||||
|
||||
private final FingerprintManager mFingerprintManager;
|
||||
private final SwirlView mIcon;
|
||||
private final TextView mErrorTextView;
|
||||
private final Callback mCallback;
|
||||
private CancellationSignal mCancellationSignal;
|
||||
|
||||
private boolean mSelfCancelled;
|
||||
|
||||
/**
|
||||
* Constructor for {@link FingerprintUiHelper}.
|
||||
*/
|
||||
public FingerprintUiHelper(FingerprintManager fingerprintManager,
|
||||
SwirlView icon, TextView errorTextView, Callback callback) {
|
||||
mFingerprintManager = fingerprintManager;
|
||||
mIcon = icon;
|
||||
mErrorTextView = errorTextView;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
public boolean isFingerprintAuthAvailable() {
|
||||
// The line below prevents the false positive inspection from Android Studio
|
||||
// noinspection ResourceType
|
||||
return mFingerprintManager.isHardwareDetected()
|
||||
&& mFingerprintManager.hasEnrolledFingerprints();
|
||||
}
|
||||
|
||||
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
|
||||
if (!isFingerprintAuthAvailable()) {
|
||||
return;
|
||||
}
|
||||
mCancellationSignal = new CancellationSignal();
|
||||
mSelfCancelled = false;
|
||||
// The line below prevents the false positive inspection from Android Studio
|
||||
// noinspection ResourceType
|
||||
mFingerprintManager
|
||||
.authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
|
||||
mIcon.setState(SwirlView.State.ON);
|
||||
}
|
||||
|
||||
public void stopListening() {
|
||||
if (mCancellationSignal != null) {
|
||||
mSelfCancelled = true;
|
||||
mCancellationSignal.cancel();
|
||||
mCancellationSignal = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
||||
if (!mSelfCancelled) {
|
||||
showError(errString);
|
||||
mIcon.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCallback.onError();
|
||||
}
|
||||
}, ERROR_TIMEOUT_MILLIS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
|
||||
showError(helpString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
showError(mIcon.getResources().getString(
|
||||
R.string.fingerprint_not_recognized));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
|
||||
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
|
||||
mIcon.setState(SwirlView.State.OFF);
|
||||
mErrorTextView.setText(
|
||||
mErrorTextView.getResources().getString(R.string.fingerprint_success));
|
||||
mIcon.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCallback.onAuthenticated();
|
||||
}
|
||||
}, SUCCESS_DELAY_MILLIS);
|
||||
|
||||
// ugly hack to keep the fingerprint icon visible while also giving visual feedback of success to the user
|
||||
mIcon.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mIcon.setState(SwirlView.State.ON);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private void showError(CharSequence error) {
|
||||
mIcon.setState(SwirlView.State.ERROR);
|
||||
mErrorTextView.setText(error);
|
||||
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
|
||||
mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
|
||||
}
|
||||
|
||||
private Runnable mResetErrorTextRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mErrorTextView.setText(
|
||||
mErrorTextView.getResources().getString(R.string.fingerprint_hint));
|
||||
mIcon.setState(SwirlView.State.ON);
|
||||
}
|
||||
};
|
||||
|
||||
public interface Callback {
|
||||
|
||||
void onAuthenticated();
|
||||
|
||||
void onError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Build;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
|
||||
public class IconViewHelper {
|
||||
private IconViewHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layer type of the given ImageView based on the given IconType. If the
|
||||
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
|
||||
* is set to hardware.
|
||||
*/
|
||||
public static void setLayerType(ImageView view, IconType iconType) {
|
||||
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
|
||||
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
|
||||
return;
|
||||
}
|
||||
|
||||
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class MetricsHelper {
|
||||
private MetricsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static int convertDpToPixels(Context context, float dp) {
|
||||
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class PasswordStrengthHelper {
|
||||
// Material design color palette
|
||||
private static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
|
||||
|
||||
public static String getString(int score, Context context) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
||||
String[] strings = context.getResources().getStringArray(R.array.password_strength);
|
||||
return strings[score];
|
||||
}
|
||||
|
||||
public static String getColor(int score) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
||||
return COLORS[score];
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ package com.beemdevelopment.aegis.helpers;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class PermissionHelper {
|
||||
private PermissionHelper() {
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.ChecksumException;
|
||||
import com.google.zxing.FormatException;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static android.graphics.ImageFormat.YUV_420_888;
|
||||
import static android.graphics.ImageFormat.YUV_422_888;
|
||||
import static android.graphics.ImageFormat.YUV_444_888;
|
||||
|
||||
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
||||
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
|
||||
public static final Size RESOLUTION = new Size(1200, 1600);
|
||||
|
||||
private final QrCodeAnalyzer.Listener _listener;
|
||||
|
||||
public QrCodeAnalyzer(QrCodeAnalyzer.Listener listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void analyze(@NonNull ImageProxy image) {
|
||||
int format = image.getFormat();
|
||||
if (format != YUV_420_888 && format != YUV_422_888 && format != YUV_444_888) {
|
||||
Log.e(TAG, String.format("Expected YUV format, got %d instead", format));
|
||||
image.close();
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] data = getLuminancePlaneData(image);
|
||||
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
|
||||
data, image.getWidth(), image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), false
|
||||
);
|
||||
|
||||
QRCodeReader reader = new QRCodeReader();
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
try {
|
||||
Result result = reader.decode(bitmap);
|
||||
if (_listener != null) {
|
||||
new Handler(Looper.getMainLooper()).post(() -> _listener.onQrCodeDetected(result));
|
||||
}
|
||||
} catch (ChecksumException | FormatException | NotFoundException ignored) {
|
||||
|
||||
} finally {
|
||||
image.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getLuminancePlaneData(ImageProxy image) {
|
||||
ImageProxy.PlaneProxy plane = image.getPlanes()[0];
|
||||
ByteBuffer buf = plane.getBuffer();
|
||||
byte[] data = new byte[buf.remaining()];
|
||||
buf.get(data);
|
||||
buf.rewind();
|
||||
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
int rowStride = plane.getRowStride();
|
||||
int pixelStride = plane.getPixelStride();
|
||||
|
||||
if (width != rowStride || pixelStride != 1) {
|
||||
// remove padding from the Y plane data
|
||||
byte[] cleanData = new byte[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
cleanData[y * width + x] = data[y * rowStride + x * pixelStride];
|
||||
}
|
||||
}
|
||||
|
||||
return cleanData;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onQrCodeDetected(Result result);
|
||||
}
|
||||
}
|
||||
+21
-2
@@ -1,10 +1,15 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
|
||||
|
||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
private VaultEntry _selectedEntry;
|
||||
|
||||
private final ItemTouchHelperAdapter _adapter;
|
||||
private boolean _positionChanged = false;
|
||||
private boolean _isLongPressDragEnabled = true;
|
||||
@@ -22,6 +27,10 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
_isLongPressDragEnabled = enabled;
|
||||
}
|
||||
|
||||
public void setSelectedEntry(VaultEntry entry) {
|
||||
_selectedEntry = entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
@@ -30,7 +39,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
@Override
|
||||
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
||||
int swipeFlags = 0;
|
||||
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
|
||||
if (adapter.getEntryAt(position) != _selectedEntry || !isLongPressDragEnabled())
|
||||
{
|
||||
dragFlags = 0;
|
||||
}
|
||||
|
||||
return makeMovementFlags(dragFlags, swipeFlags);
|
||||
}
|
||||
|
||||
@@ -56,4 +73,6 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
_positionChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.ArrayRes;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SpinnerHelper {
|
||||
private SpinnerHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void fillSpinner(Context context, Spinner spinner, @ArrayRes int textArrayResId) {
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, android.R.layout.simple_spinner_item);
|
||||
initSpinner(spinner, adapter);
|
||||
}
|
||||
|
||||
public static <T> void fillSpinner(Context context, Spinner spinner, List<T> items) {
|
||||
ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, items);
|
||||
initSpinner(spinner, adapter);
|
||||
}
|
||||
|
||||
private static void initSpinner(Spinner spinner, ArrayAdapter adapter) {
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.invalidate();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import me.dm7.barcodescanner.core.ViewFinderView;
|
||||
|
||||
public class SquareFinderView extends ViewFinderView {
|
||||
|
||||
public SquareFinderView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public SquareFinderView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setSquareViewFinder(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,6 @@ public class TextDrawableHelper {
|
||||
.width(view.getLayoutParams().width)
|
||||
.height(view.getLayoutParams().height)
|
||||
.endConfig()
|
||||
.buildRect(text.substring(0, 1).toUpperCase(), color);
|
||||
.buildRound(text.substring(0, 1).toUpperCase(), color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class ThemeHelper {
|
||||
private ThemeHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
currentTheme.resolveAttribute(attributeId, typedValue, true);
|
||||
@ColorInt int color = typedValue.data;
|
||||
|
||||
return color;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ public class UiRefresher {
|
||||
_handler = new Handler();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
stop();
|
||||
_listener = null;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (_running) {
|
||||
return;
|
||||
@@ -22,15 +27,14 @@ public class UiRefresher {
|
||||
_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (_running) {
|
||||
_listener.onRefresh();
|
||||
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
_listener.onRefresh();
|
||||
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
}, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
_handler.removeCallbacksAndMessages(null);
|
||||
_running = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class UiThreadExecutor implements Executor {
|
||||
private final Handler _handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
public void execute(@NonNull Runnable command) {
|
||||
_handler.post(command);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class AccountNameComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return a.getName().compareToIgnoreCase(b.getName());
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class IssuerNameComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class UsageCountComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return Integer.compare(a.getUsageCount(), b.getUsageCount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconPack {
|
||||
private UUID _uuid;
|
||||
private String _name;
|
||||
private int _version;
|
||||
private List<Icon> _icons;
|
||||
|
||||
private File _dir;
|
||||
|
||||
private IconPack(UUID uuid, String name, int version, List<Icon> icons) {
|
||||
_uuid = uuid;
|
||||
_name = name;
|
||||
_version = version;
|
||||
_icons = icons;
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return _version;
|
||||
}
|
||||
|
||||
public List<Icon> getIcons() {
|
||||
return Collections.unmodifiableList(_icons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of icons suggested for the given issuer.
|
||||
*/
|
||||
public List<Icon> getSuggestedIcons(String issuer) {
|
||||
if (issuer == null || issuer.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return _icons.stream()
|
||||
.filter(i -> i.isSuggestedFor(issuer))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public File getDirectory() {
|
||||
return _dir;
|
||||
}
|
||||
|
||||
void setDirectory(@NonNull File dir) {
|
||||
_dir = dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one. The object does not
|
||||
* necessarily have to be the same instance. Equality of UUID and version will make
|
||||
* this method return true;
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof IconPack)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IconPack pack = (IconPack) o;
|
||||
return super.equals(pack) || (getUUID().equals(pack.getUUID()) && getVersion() == pack.getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(_uuid, _version);
|
||||
}
|
||||
|
||||
public static IconPack fromJson(JSONObject obj) throws JSONException {
|
||||
UUID uuid;
|
||||
String uuidString = obj.getString("uuid");
|
||||
try {
|
||||
uuid = UUID.fromString(uuidString);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new JSONException(String.format("Bad UUID format: %s", uuidString));
|
||||
}
|
||||
String name = obj.getString("name");
|
||||
int version = obj.getInt("version");
|
||||
JSONArray array = obj.getJSONArray("icons");
|
||||
|
||||
List<Icon> icons = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
Icon icon = Icon.fromJson(array.getJSONObject(i));
|
||||
icons.add(icon);
|
||||
}
|
||||
|
||||
return new IconPack(uuid, name, version, icons);
|
||||
}
|
||||
|
||||
public static IconPack fromBytes(byte[] data) throws JSONException {
|
||||
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
|
||||
return IconPack.fromJson(obj);
|
||||
}
|
||||
|
||||
public static class Icon implements Serializable {
|
||||
private final String _relFilename;
|
||||
private final String _category;
|
||||
private final List<String> _issuers;
|
||||
|
||||
private File _file;
|
||||
|
||||
protected Icon(String filename, String category, List<String> issuers) {
|
||||
_relFilename = filename;
|
||||
_category = category;
|
||||
_issuers = issuers;
|
||||
}
|
||||
|
||||
public String getRelativeFilename() {
|
||||
return _relFilename;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public File getFile() {
|
||||
return _file;
|
||||
}
|
||||
|
||||
void setFile(@NonNull File file) {
|
||||
_file = file;
|
||||
}
|
||||
|
||||
public IconType getIconType() {
|
||||
return IconType.fromFilename(_relFilename);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public String getName() {
|
||||
return Files.getNameWithoutExtension(new File(_relFilename).getName());
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return _category;
|
||||
}
|
||||
|
||||
public List<String> getIssuers() {
|
||||
return Collections.unmodifiableList(_issuers);
|
||||
}
|
||||
|
||||
public boolean isSuggestedFor(String issuer) {
|
||||
String lowerIssuer = issuer.toLowerCase();
|
||||
return getIssuers().stream()
|
||||
.map(String::toLowerCase)
|
||||
.anyMatch(is -> is.contains(lowerIssuer) || lowerIssuer.contains(is));
|
||||
}
|
||||
|
||||
public static Icon fromJson(JSONObject obj) throws JSONException {
|
||||
String filename = obj.getString("filename");
|
||||
String category = obj.isNull("category") ? null : obj.getString("category");
|
||||
JSONArray array = obj.getJSONArray("issuer");
|
||||
|
||||
List<String> issuers = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
String issuer = array.getString(i);
|
||||
issuers.add(issuer);
|
||||
}
|
||||
|
||||
return new Icon(filename, category, issuers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
public class IconPackException extends Exception {
|
||||
public IconPackException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public IconPackException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
public class IconPackExistsException extends IconPackException {
|
||||
private IconPack _pack;
|
||||
|
||||
public IconPackExistsException(IconPack pack) {
|
||||
super(String.format("Icon pack %s (%d) already exists", pack.getName(), pack.getVersion()));
|
||||
_pack = pack;
|
||||
}
|
||||
|
||||
public IconPack getIconPack() {
|
||||
return _pack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import net.lingala.zip4j.ZipFile;
|
||||
import net.lingala.zip4j.io.inputstream.ZipInputStream;
|
||||
import net.lingala.zip4j.model.FileHeader;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconPackManager {
|
||||
private static final String _packDefFilename = "pack.json";
|
||||
|
||||
private File _iconsBaseDir;
|
||||
private List<IconPack> _iconPacks;
|
||||
|
||||
public IconPackManager(Context context) {
|
||||
_iconPacks = new ArrayList<>();
|
||||
_iconsBaseDir = new File(context.getFilesDir(), "icons");
|
||||
rescanIconPacks();
|
||||
}
|
||||
|
||||
private IconPack getIconPackByUUID(UUID uuid) {
|
||||
List<IconPack> packs = _iconPacks.stream().filter(i -> i.getUUID().equals(uuid)).collect(Collectors.toList());
|
||||
if (packs.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return packs.get(0);
|
||||
}
|
||||
|
||||
public List<IconPack> getIconPacks() {
|
||||
return new ArrayList<>(_iconPacks);
|
||||
}
|
||||
|
||||
public void removeIconPack(IconPack pack) throws IconPackException {
|
||||
try {
|
||||
File dir = getIconPackDir(pack);
|
||||
deleteDir(dir);
|
||||
} catch (IOException e) {
|
||||
throw new IconPackException(e);
|
||||
}
|
||||
|
||||
_iconPacks.remove(pack);
|
||||
}
|
||||
|
||||
public IconPack importPack(File inFile) throws IconPackException {
|
||||
try {
|
||||
// read and parse the icon pack definition file of the icon pack
|
||||
ZipFile zipFile = new ZipFile(inFile);
|
||||
FileHeader packHeader = zipFile.getFileHeader(_packDefFilename);
|
||||
if (packHeader == null) {
|
||||
throw new IOException("Unable to find pack.json in the root of the ZIP file");
|
||||
}
|
||||
IconPack pack;
|
||||
byte[] defBytes;
|
||||
try (ZipInputStream inStream = zipFile.getInputStream(packHeader)) {
|
||||
defBytes = IOUtils.readAll(inStream);
|
||||
pack = IconPack.fromBytes(defBytes);
|
||||
}
|
||||
|
||||
// create a new directory to store the icon pack, based on the UUID and version
|
||||
File packDir = getIconPackDir(pack);
|
||||
if (!packDir.getCanonicalPath().startsWith(_iconsBaseDir.getCanonicalPath() + File.separator)) {
|
||||
throw new IOException("Attempted to write outside of the parent directory");
|
||||
}
|
||||
if (packDir.exists()) {
|
||||
throw new IconPackExistsException(pack);
|
||||
}
|
||||
IconPack existingPack = getIconPackByUUID(pack.getUUID());
|
||||
if (existingPack != null) {
|
||||
throw new IconPackExistsException(existingPack);
|
||||
}
|
||||
if (!packDir.exists() && !packDir.mkdirs()) {
|
||||
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
|
||||
}
|
||||
|
||||
// extract each of the defined icons to the icon pack directory
|
||||
for (IconPack.Icon icon : pack.getIcons()) {
|
||||
File destFile = new File(packDir, icon.getRelativeFilename());
|
||||
FileHeader iconHeader = zipFile.getFileHeader(icon.getRelativeFilename());
|
||||
if (iconHeader == null) {
|
||||
throw new IOException(String.format("Unable to find %s relative to the root of the ZIP file", icon.getRelativeFilename()));
|
||||
}
|
||||
|
||||
// create new directories for this file if needed
|
||||
File parent = destFile.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
|
||||
}
|
||||
|
||||
try (ZipInputStream inStream = zipFile.getInputStream(iconHeader);
|
||||
FileOutputStream outStream = new FileOutputStream(destFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
}
|
||||
|
||||
// after successful copy of the icon, store the new filename
|
||||
icon.setFile(destFile);
|
||||
}
|
||||
|
||||
// write the icon pack definition file to the newly created directory
|
||||
try (FileOutputStream outStream = new FileOutputStream(new File(packDir, _packDefFilename))) {
|
||||
outStream.write(defBytes);
|
||||
}
|
||||
|
||||
// after successful extraction of the icon pack, store the new directory
|
||||
pack.setDirectory(packDir);
|
||||
_iconPacks.add(pack);
|
||||
return pack;
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new IconPackException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void rescanIconPacks() {
|
||||
_iconPacks.clear();
|
||||
|
||||
File[] dirs = _iconsBaseDir.listFiles();
|
||||
if (dirs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File dir : dirs) {
|
||||
if (!dir.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UUID uuid;
|
||||
try {
|
||||
uuid = UUID.fromString(dir.getName());
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
|
||||
File versionDir = getLatestVersionDir(dir);
|
||||
if (versionDir != null) {
|
||||
IconPack pack;
|
||||
try (FileInputStream inStream = new FileInputStream(new File(versionDir, _packDefFilename))) {
|
||||
byte[] bytes = IOUtils.readAll(inStream);
|
||||
pack = IconPack.fromBytes(bytes);
|
||||
pack.setDirectory(versionDir);
|
||||
} catch (JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (IconPack.Icon icon : pack.getIcons()) {
|
||||
icon.setFile(new File(versionDir, icon.getRelativeFilename()));
|
||||
}
|
||||
|
||||
// do a sanity check on the UUID and version
|
||||
if (pack.getUUID().equals(uuid) && pack.getVersion() == Integer.parseInt(versionDir.getName())) {
|
||||
_iconPacks.add(pack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File getIconPackDir(IconPack pack) {
|
||||
return new File(_iconsBaseDir, pack.getUUID() + File.separator + pack.getVersion());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static File getLatestVersionDir(File packDir) {
|
||||
File[] dirs = packDir.listFiles();
|
||||
if (dirs == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int latestVersion = -1;
|
||||
for (File versionDir : dirs) {
|
||||
int version;
|
||||
try {
|
||||
version = Integer.parseInt(versionDir.getName());
|
||||
} catch (NumberFormatException ignored) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (latestVersion == -1 || version > latestVersion) {
|
||||
latestVersion = version;
|
||||
}
|
||||
}
|
||||
|
||||
if (latestVersion == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File(packDir, Integer.toString(latestVersion));
|
||||
}
|
||||
|
||||
private static void deleteDir(File dir) throws IOException {
|
||||
if (dir.isDirectory()) {
|
||||
File[] children = dir.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteDir(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!dir.delete()) {
|
||||
throw new IOException(String.format("Unable to delete directory: %s", dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum IconType {
|
||||
INVALID,
|
||||
SVG,
|
||||
PNG,
|
||||
JPEG;
|
||||
|
||||
public static IconType fromMimeType(String mimeType) {
|
||||
switch (mimeType) {
|
||||
case "image/svg+xml":
|
||||
return SVG;
|
||||
case "image/png":
|
||||
return PNG;
|
||||
case "image/jpeg":
|
||||
return JPEG;
|
||||
default:
|
||||
return INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public static IconType fromFilename(String filename) {
|
||||
switch (Files.getFileExtension(filename).toLowerCase(Locale.ROOT)) {
|
||||
case "svg":
|
||||
return SVG;
|
||||
case "png":
|
||||
return PNG;
|
||||
case "jpg":
|
||||
// intentional fallthrough
|
||||
case "jpeg":
|
||||
return JPEG;
|
||||
default:
|
||||
return INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
public String toMimeType() {
|
||||
switch (this) {
|
||||
case SVG:
|
||||
return "image/svg+xml";
|
||||
case PNG:
|
||||
return "image/png";
|
||||
case JPEG:
|
||||
return "image/jpeg";
|
||||
default:
|
||||
throw new RuntimeException(String.format("Can't convert icon type %s to MIME type", this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.beemdevelopment.aegis.db.Database;
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.db.DatabaseException;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFile;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileException;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
|
||||
public class AegisFileImporter extends DatabaseFileImporter {
|
||||
private DatabaseFileCredentials _creds;
|
||||
private DatabaseFile _file;
|
||||
|
||||
public AegisFileImporter(Context context, ByteInputStream stream) {
|
||||
super(context, stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse() throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = _stream.getBytes();
|
||||
_file = DatabaseFile.fromBytes(bytes);
|
||||
} catch (DatabaseFileException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DatabaseEntry> convert() throws DatabaseImporterException {
|
||||
try {
|
||||
JSONObject obj;
|
||||
if (_file.isEncrypted() && _creds != null) {
|
||||
obj = _file.getContent(_creds);
|
||||
} else {
|
||||
obj = _file.getContent();
|
||||
}
|
||||
|
||||
Database db = Database.fromJson(obj);
|
||||
return db.getEntries();
|
||||
} catch (DatabaseException | DatabaseFileException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() {
|
||||
return _file.isEncrypted();
|
||||
}
|
||||
|
||||
public void setCredentials(DatabaseFileCredentials creds) {
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public DatabaseFile getFile() {
|
||||
return _file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.beemdevelopment.aegis.helpers.ContextHelper;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryException;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class AegisImporter extends DatabaseImporter {
|
||||
|
||||
public AegisImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
VaultFile file = VaultFile.fromBytes(bytes);
|
||||
if (file.isEncrypted()) {
|
||||
return new EncryptedState(file);
|
||||
}
|
||||
return new DecryptedState(file.getContent());
|
||||
} catch (VaultFileException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EncryptedState extends State {
|
||||
private VaultFile _file;
|
||||
|
||||
private EncryptedState(VaultFile file) {
|
||||
super(true);
|
||||
_file = file;
|
||||
}
|
||||
|
||||
public SlotList getSlots() {
|
||||
return _file.getHeader().getSlots();
|
||||
}
|
||||
|
||||
public State decrypt(VaultFileCredentials creds) throws DatabaseImporterException {
|
||||
JSONObject obj;
|
||||
try {
|
||||
obj = _file.getContent(creds);
|
||||
} catch (VaultFileException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new DecryptedState(obj);
|
||||
}
|
||||
|
||||
public State decrypt(char[] password) throws DatabaseImporterException {
|
||||
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Result result = PasswordSlotDecryptTask.decrypt(slots, password);
|
||||
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
|
||||
return decrypt(creds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, (Dialogs.TextInputListener) password -> {
|
||||
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(context, result -> {
|
||||
try {
|
||||
if (result == null) {
|
||||
throw new DatabaseImporterException("Password incorrect");
|
||||
}
|
||||
|
||||
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
|
||||
State state = decrypt(creds);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}, (DialogInterface.OnCancelListener) dialog -> listener.onCanceled());
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends State {
|
||||
private JSONObject _obj;
|
||||
|
||||
private DecryptedState(JSONObject obj) {
|
||||
super(false);
|
||||
_obj = obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
JSONArray array = _obj.getJSONArray("entries");
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject entryObj = array.getJSONObject(i);
|
||||
try {
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
return VaultEntry.fromJson(obj);
|
||||
} catch (VaultEntryException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
|
||||
public class AndOtpFileImporter extends DatabaseFileImporter {
|
||||
private JSONArray _obj;
|
||||
|
||||
public AndOtpFileImporter(Context context, ByteInputStream stream) {
|
||||
super(context, stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse() throws DatabaseImporterException {
|
||||
try {
|
||||
_obj = new JSONArray(new String(_stream.getBytes(), "UTF-8"));
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DatabaseEntry> convert() throws DatabaseImporterException {
|
||||
List<DatabaseEntry> entries = new ArrayList<>();
|
||||
|
||||
try {
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
JSONObject obj = _obj.getJSONObject(i);
|
||||
|
||||
String type = obj.getString("type").toLowerCase();
|
||||
String algo = obj.getString("algorithm");
|
||||
int digits = obj.getInt("digits");
|
||||
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
|
||||
|
||||
OtpInfo info;
|
||||
if (type.equals("totp")) {
|
||||
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
|
||||
} else if (type.equals("hotp")) {
|
||||
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
|
||||
} else {
|
||||
throw new DatabaseImporterException("unsupported otp type: " + type);
|
||||
}
|
||||
|
||||
String issuer = "";
|
||||
String name = "";
|
||||
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
} else {
|
||||
name = parts[0];
|
||||
}
|
||||
|
||||
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
|
||||
entries.add(entry);
|
||||
}
|
||||
} catch (Base32Exception | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.helpers.ContextHelper;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AndOtpImporter extends DatabaseImporter {
|
||||
private static final int INT_SIZE = 4;
|
||||
private static final int NONCE_SIZE = 12;
|
||||
private static final int TAG_SIZE = 16;
|
||||
private static final int SALT_SIZE = 12;
|
||||
private static final int KEY_SIZE = 256; // bits
|
||||
|
||||
public AndOtpImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = IOUtils.readAll(stream);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
return read(bytes);
|
||||
} catch (JSONException e) {
|
||||
// andOTP doesn't have a proper way to indicate whether a file is encrypted
|
||||
// so, if we can't parse it as JSON, we'll have to assume it is
|
||||
return new EncryptedState(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static DecryptedState read(byte[] bytes) throws JSONException {
|
||||
JSONArray array = new JSONArray(new String(bytes, StandardCharsets.UTF_8));
|
||||
return new DecryptedState(array);
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private byte[] _data;
|
||||
|
||||
public EncryptedState(byte[] data) {
|
||||
super(true);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
private DecryptedState decryptContent(SecretKey key, int offset) throws DatabaseImporterException {
|
||||
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
|
||||
try {
|
||||
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
|
||||
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
|
||||
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
|
||||
return read(result.getData());
|
||||
} catch (IOException | BadPaddingException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} catch (NoSuchAlgorithmException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| NoSuchPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyDerivationParams getKeyDerivationParams(char[] password) throws DatabaseImporterException {
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, INT_SIZE + SALT_SIZE);
|
||||
return new KeyDerivationParams(password, salt, iterations);
|
||||
}
|
||||
|
||||
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash;
|
||||
try {
|
||||
hash = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
return decryptContent(key, 0);
|
||||
}
|
||||
|
||||
protected DecryptedState decryptNewFormat(SecretKey key) throws DatabaseImporterException {
|
||||
return decryptContent(key, INT_SIZE + SALT_SIZE);
|
||||
}
|
||||
|
||||
protected DecryptedState decryptNewFormat(char[] password)
|
||||
throws DatabaseImporterException {
|
||||
KeyDerivationParams params = getKeyDerivationParams(password);
|
||||
SecretKey key = AndOtpKeyDerivationTask.deriveKey(params);
|
||||
return decryptNewFormat(key);
|
||||
}
|
||||
|
||||
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
|
||||
if (oldFormat) {
|
||||
DecryptedState state = decryptOldFormat(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} else {
|
||||
KeyDerivationParams params = getKeyDerivationParams(password);
|
||||
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key -> {
|
||||
try {
|
||||
DecryptedState state = decryptNewFormat(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
String[] choices = new String[]{
|
||||
context.getResources().getString(R.string.andotp_new_format),
|
||||
context.getResources().getString(R.string.andotp_old_format)
|
||||
};
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.choose_andotp_importer)
|
||||
.setSingleChoiceItems(choices, 0, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try {
|
||||
decrypt(context, password, i != 0, listener);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
})
|
||||
.create());
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends DatabaseImporter.State {
|
||||
private JSONArray _obj;
|
||||
|
||||
private DecryptedState(JSONArray obj) {
|
||||
super(false);
|
||||
_obj = obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = new Result();
|
||||
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
try {
|
||||
JSONObject obj = _obj.getJSONObject(i);
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
String type = obj.getString("type").toLowerCase(Locale.ROOT);
|
||||
String algo = obj.getString("algorithm");
|
||||
int digits = obj.getInt("digits");
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
|
||||
OtpInfo info;
|
||||
switch (type) {
|
||||
case "hotp":
|
||||
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
|
||||
break;
|
||||
case "totp":
|
||||
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
|
||||
break;
|
||||
case "steam":
|
||||
info = new SteamInfo(secret, algo, digits, obj.optInt("period", TotpInfo.DEFAULT_PERIOD));
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterException("unsupported otp type: " + type);
|
||||
}
|
||||
|
||||
String name;
|
||||
String issuer = "";
|
||||
|
||||
if (obj.has("issuer")) {
|
||||
name = obj.getString("label");
|
||||
issuer = obj.getString("issuer");
|
||||
} else {
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
} else {
|
||||
name = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpImporter.KeyDerivationParams, SecretKey> {
|
||||
private Callback _cb;
|
||||
|
||||
public AndOtpKeyDerivationTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.unlocking_vault));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey doInBackground(AndOtpImporter.KeyDerivationParams... args) {
|
||||
setPriority();
|
||||
|
||||
AndOtpImporter.KeyDerivationParams params = args[0];
|
||||
return deriveKey(params);
|
||||
}
|
||||
|
||||
protected static SecretKey deriveKey(KeyDerivationParams params) {
|
||||
try {
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
return new SecretKeySpec(key.getEncoded(), "AES");
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(SecretKey key) {
|
||||
super.onPostExecute(key);
|
||||
_cb.onTaskFinished(key);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class KeyDerivationParams {
|
||||
private final char[] _password;
|
||||
private final byte[] _salt;
|
||||
private final int _iterations;
|
||||
|
||||
public KeyDerivationParams(char[] password, byte[] salt, int iterations) {
|
||||
_iterations = iterations;
|
||||
_password = password;
|
||||
_salt = salt;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return _iterations;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return _salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import net.lingala.zip4j.io.inputstream.ZipInputStream;
|
||||
import net.lingala.zip4j.model.LocalFileHeader;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AuthenticatorPlusImporter extends DatabaseImporter {
|
||||
private static final String FILENAME = "Accounts.txt";
|
||||
|
||||
public AuthenticatorPlusImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
return new EncryptedState(IOUtils.readAll(stream));
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private final byte[] _data;
|
||||
|
||||
private EncryptedState(byte[] data) {
|
||||
super(true);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
protected State decrypt(char[] password) throws DatabaseImporterException {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
|
||||
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
|
||||
LocalFileHeader header;
|
||||
while ((header = zipStream.getNextEntry()) != null) {
|
||||
File file = new File(header.getFileName());
|
||||
if (file.getName().equals(FILENAME)) {
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(null);
|
||||
return importer.read(zipStream);
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(FILENAME);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try {
|
||||
DatabaseImporter.State state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AuthyImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs";
|
||||
private static final String _pkgName = "com.authy.authy";
|
||||
private static final String _authFilename = "com.authy.storage.tokens.authenticator";
|
||||
private static final String _authyFilename = "com.authy.storage.tokens.authy";
|
||||
|
||||
private static final int ITERATIONS = 1000;
|
||||
private static final int KEY_SIZE = 256;
|
||||
private static final byte[] IV = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
public AuthyImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
|
||||
JSONArray array;
|
||||
JSONArray authyArray;
|
||||
try {
|
||||
array = readFile(new SuFile(path, String.format("%s.xml", _authFilename)), String.format("%s.key", _authFilename));
|
||||
authyArray = readFile(new SuFile(path, String.format("%s.xml", _authyFilename)), String.format("%s.key", _authyFilename));
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < authyArray.length(); i++) {
|
||||
array.put(authyArray.getJSONObject(i));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return read(array);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
JSONArray array = new JSONArray();
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals(String.format("%s.key", _authFilename))
|
||||
|| entry.Name.equals(String.format("%s.key", _authyFilename))) {
|
||||
array = new JSONArray(entry.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return read(array);
|
||||
} catch (XmlPullParserException | JSONException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private State read(JSONArray array) throws DatabaseImporterException {
|
||||
try {
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject obj = array.getJSONObject(i);
|
||||
if (!obj.has("decryptedSecret") && !obj.has("secretSeed")) {
|
||||
return new EncryptedState(array);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new DecryptedState(array);
|
||||
}
|
||||
|
||||
private JSONArray readFile(SuFile file, String key) throws IOException, XmlPullParserException {
|
||||
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(inStream, null);
|
||||
parser.nextTag();
|
||||
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals(key)) {
|
||||
return new JSONArray(entry.Value);
|
||||
}
|
||||
}
|
||||
} catch (JSONException ignored) {
|
||||
|
||||
}
|
||||
|
||||
return new JSONArray();
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private JSONArray _array;
|
||||
|
||||
private EncryptedState(JSONArray array) {
|
||||
super(true);
|
||||
_array = array;
|
||||
}
|
||||
|
||||
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
try {
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject obj = _array.getJSONObject(i);
|
||||
String secretString = JsonUtils.optString(obj, "encryptedSecret");
|
||||
if (secretString == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] encryptedSecret = Base64.decode(secretString);
|
||||
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
key = new SecretKeySpec(key.getEncoded(), "AES");
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
|
||||
|
||||
byte[] secret = cipher.doFinal(encryptedSecret);
|
||||
obj.remove("encryptedSecret");
|
||||
obj.remove("salt");
|
||||
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
return new DecryptedState(_array);
|
||||
} catch (JSONException
|
||||
| EncodingException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidKeySpecException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| NoSuchPaddingException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
|
||||
try {
|
||||
DecryptedState state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends DatabaseImporter.State {
|
||||
private JSONArray _array;
|
||||
|
||||
private DecryptedState(JSONArray array) {
|
||||
super(false);
|
||||
_array = array;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject entryObj = _array.getJSONObject(i);
|
||||
try {
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
|
||||
authyEntryInfo.OriginalName = JsonUtils.optString(entry, "originalName");
|
||||
authyEntryInfo.OriginalIssuer = JsonUtils.optString(entry, "originalIssuer");
|
||||
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
|
||||
authyEntryInfo.Name = entry.optString("name");
|
||||
|
||||
boolean isAuthy = entry.has("secretSeed");
|
||||
sanitizeEntryInfo(authyEntryInfo, isAuthy);
|
||||
|
||||
byte[] secret;
|
||||
if (isAuthy) {
|
||||
secret = Hex.decode(entry.getString("secretSeed"));
|
||||
} else {
|
||||
secret = Base32.decode(entry.getString("decryptedSecret"));
|
||||
}
|
||||
|
||||
int digits = entry.getInt("digits");
|
||||
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, isAuthy ? 10 : TotpInfo.DEFAULT_PERIOD);
|
||||
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
|
||||
} catch (OtpInfoException | JSONException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void sanitizeEntryInfo(AuthyEntryInfo info, boolean isAuthy) {
|
||||
if (!isAuthy) {
|
||||
String separator = "";
|
||||
|
||||
if (info.OriginalIssuer != null) {
|
||||
info.Issuer = info.OriginalIssuer;
|
||||
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
|
||||
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
|
||||
separator = ":";
|
||||
} else if (info.Name.contains(" - ")) {
|
||||
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
|
||||
separator = " - ";
|
||||
} else {
|
||||
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
|
||||
}
|
||||
|
||||
info.Name = info.Name.replace(info.Issuer + separator, "");
|
||||
} else {
|
||||
info.Issuer = info.Name;
|
||||
info.Name = "";
|
||||
}
|
||||
|
||||
if (info.Name.startsWith(": ")) {
|
||||
info.Name = info.Name.substring(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthyEntryInfo {
|
||||
String OriginalName;
|
||||
String OriginalIssuer;
|
||||
String AccountType;
|
||||
String Issuer;
|
||||
String Name;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class DatabaseAppImporter implements DatabaseImporter {
|
||||
private static Map<String, Class<? extends DatabaseAppImporter>> _importers;
|
||||
static {
|
||||
// note: keep this list sorted alphabetically
|
||||
LinkedHashMap<String, Class<? extends DatabaseAppImporter>> importers = new LinkedHashMap<>();
|
||||
importers.put("Google Authenticator", GoogleAuthAppImporter.class);
|
||||
_importers = Collections.unmodifiableMap(importers);
|
||||
}
|
||||
|
||||
private Context _context;
|
||||
|
||||
protected DatabaseAppImporter(Context context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public abstract void parse() throws DatabaseImporterException;
|
||||
|
||||
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
|
||||
|
||||
public abstract boolean isEncrypted();
|
||||
|
||||
public Context getContext() {
|
||||
return _context;
|
||||
}
|
||||
|
||||
public static DatabaseAppImporter create(Context context, Class<? extends DatabaseAppImporter> type) {
|
||||
try {
|
||||
return type.getConstructor(Context.class).newInstance(context);
|
||||
} catch (IllegalAccessException | InstantiationException
|
||||
| NoSuchMethodException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Class<? extends DatabaseAppImporter>> getImporters() {
|
||||
return _importers;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
|
||||
public abstract class DatabaseFileImporter implements DatabaseImporter {
|
||||
private static Map<String, Class<? extends DatabaseFileImporter>> _importers;
|
||||
static {
|
||||
// note: keep this list sorted alphabetically
|
||||
LinkedHashMap<String, Class<? extends DatabaseFileImporter>> importers = new LinkedHashMap<>();
|
||||
importers.put("Aegis", AegisFileImporter.class);
|
||||
importers.put("andOTP", AndOtpFileImporter.class);
|
||||
importers.put("FreeOTP", FreeOtpFileImporter.class);
|
||||
_importers = Collections.unmodifiableMap(importers);
|
||||
}
|
||||
|
||||
private Context _context;
|
||||
protected ByteInputStream _stream;
|
||||
|
||||
protected DatabaseFileImporter(Context context, ByteInputStream stream) {
|
||||
_context = context;
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
public abstract void parse() throws DatabaseImporterException;
|
||||
|
||||
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
|
||||
|
||||
public abstract boolean isEncrypted();
|
||||
|
||||
public Context getContext() {
|
||||
return _context;
|
||||
}
|
||||
|
||||
public static DatabaseFileImporter create(Context context, ByteInputStream stream, Class<? extends DatabaseFileImporter> type) {
|
||||
try {
|
||||
return type.getConstructor(Context.class, ByteInputStream.class).newInstance(context, stream);
|
||||
} catch (IllegalAccessException | InstantiationException
|
||||
| NoSuchMethodException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Class<? extends DatabaseFileImporter>> getImporters() {
|
||||
return _importers;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user