Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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
|
||||
@@ -39,3 +39,4 @@ captures/
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
crowdin.properties
|
||||
|
||||
+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,58 +1,65 @@
|
||||
# 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)
|
||||
|
||||
Aegis' security design and vault format is described in detail in [this document](docs/vault.md).
|
||||
__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.
|
||||
|
||||
# Features
|
||||
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 from files
|
||||
- andOTP
|
||||
- FreeOTP
|
||||
- Aegis
|
||||
- Import from apps (requires root):
|
||||
- Google Authenticator
|
||||
- Steam
|
||||
- Supported algorithms:
|
||||
- HOTP ([RFC 4226](https://tools.ietf.org/html/rfc4226))
|
||||
- TOTP ([RFC 6238](https://tools.ietf.org/html/rfc6238))
|
||||
- Steam ([RFC 6238](https://tools.ietf.org/html/rfc6238) with custom
|
||||
encoding)
|
||||
- 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 with multiple themes:
|
||||
- Light theme
|
||||
- Dark theme
|
||||
- Amoled / true dark theme
|
||||
- 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)
|
||||
[<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
|
||||
|
||||
@@ -96,25 +103,38 @@ Certificate fingerprints:
|
||||
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
|
||||
```
|
||||
|
||||
## Libraries
|
||||
### Icon packs
|
||||
|
||||
- [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
|
||||
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.
|
||||
|
||||
## Attribution
|
||||
|
||||
The icon is largely based on a design by Freepik from Flaticon.
|
||||
|
||||
+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 12
|
||||
versionName "0.4.2"
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 48
|
||||
versionName "2.0"
|
||||
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:0.44'
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -4,50 +4,116 @@
|
||||
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.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=".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"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
android:theme="@style/Theme.Aegis.Launch"
|
||||
tools:replace="android:theme">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
<activity
|
||||
android:name=".ui.SelectEntriesActivity"
|
||||
android:label="Select entries" />
|
||||
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}"
|
||||
android:launchMode="singleTask">
|
||||
android:label="${title}">
|
||||
<intent-filter>
|
||||
<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=".ui.ScannerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="Scan a QR code"
|
||||
android:label="@string/title_activity_scan_qr"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.EditEntryActivity"
|
||||
android:label="Edit profile" />
|
||||
android:label="@string/title_activity_edit_entry" />
|
||||
<activity
|
||||
android:name=".ui.IntroActivity"
|
||||
android:theme="@style/Theme.Intro" />
|
||||
<activity android:name=".ui.AuthActivity" />
|
||||
<activity android:name=".ui.PreferencesActivity" />
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.AuthActivity" />
|
||||
<activity
|
||||
android:name=".ui.PreferencesActivity"
|
||||
android:label="@string/title_activity_preferences" />
|
||||
<activity
|
||||
android:name=".ui.SlotManagerActivity"
|
||||
android:label="Manage key slots" />
|
||||
android:label="@string/title_activity_manage_slots" />
|
||||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="Manage groups" />
|
||||
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>
|
||||
|
||||
</manifest>
|
||||
<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,247 @@
|
||||
<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</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,6 +1,8 @@
|
||||
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;
|
||||
@@ -10,46 +12,133 @@ import android.content.pm.ShortcutManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||
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;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
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();
|
||||
registerReceiver(receiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
|
||||
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() {
|
||||
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
|
||||
public boolean isAutoLockEnabled(int autoLockType) {
|
||||
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
|
||||
}
|
||||
|
||||
public void registerLockListener(LockListener listener) {
|
||||
@@ -60,11 +149,28 @@ public class AegisApplication extends Application {
|
||||
_lockListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
_manager.lock();
|
||||
/**
|
||||
* 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();
|
||||
listener.onLocked(userInitiated);
|
||||
}
|
||||
|
||||
stopService(new Intent(AegisApplication.this, NotificationService.class));
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
|
||||
@@ -76,28 +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()) {
|
||||
lock();
|
||||
if (isAutoLockEnabled(Preferences.AUTO_LOCK_ON_DEVICE_LOCK)) {
|
||||
lock(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface LockListener {
|
||||
void onLocked();
|
||||
/**
|
||||
* 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,33 +2,113 @@ 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 java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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);
|
||||
|
||||
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 _prefs.getBoolean("pref_auto_lock", true);
|
||||
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) {
|
||||
@@ -52,7 +132,7 @@ public class Preferences {
|
||||
}
|
||||
|
||||
public Theme getCurrentTheme() {
|
||||
return Theme.fromInteger(_prefs.getInt("pref_current_theme", 0));
|
||||
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
|
||||
}
|
||||
|
||||
public void setCurrentTheme(Theme theme) {
|
||||
@@ -70,4 +150,107 @@ public class Preferences {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
|
||||
|
||||
@@ -24,8 +24,8 @@ public enum SortCategory {
|
||||
return _values[x];
|
||||
}
|
||||
|
||||
public Comparator<DatabaseEntry> getComparator() {
|
||||
Comparator<DatabaseEntry> comparator = null;
|
||||
public Comparator<VaultEntry> getComparator() {
|
||||
Comparator<VaultEntry> comparator = null;
|
||||
|
||||
switch (this) {
|
||||
case ACCOUNT:
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.beemdevelopment.aegis;
|
||||
public enum Theme {
|
||||
LIGHT,
|
||||
DARK,
|
||||
AMOLED;
|
||||
AMOLED,
|
||||
SYSTEM,
|
||||
SYSTEM_AMOLED;
|
||||
|
||||
private static Theme[] _values;
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -30,4 +30,15 @@ public enum ViewMode {
|
||||
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,7 +2,7 @@ package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import org.spongycastle.crypto.generators.SCrypt;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -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,6 +13,7 @@ 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;
|
||||
@@ -21,8 +23,6 @@ import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class KeyStoreHandle {
|
||||
private final KeyStore _keyStore;
|
||||
private static final String STORE_NAME = "AndroidKeyStore";
|
||||
@@ -61,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);
|
||||
}
|
||||
@@ -88,18 +96,14 @@ public class KeyStoreHandle {
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
|
||||
// try to initialize a dummy cipher
|
||||
// and see if KeyPermanentlyInvalidatedException is thrown
|
||||
// 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 (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
// apparently KitKat doesn't like KeyPermanentlyInvalidatedException, even when guarded with a version check
|
||||
// it will throw a java.lang.VerifyError when its listed in a 'catch' statement
|
||||
// so instead, check for it here
|
||||
if (e instanceof KeyPermanentlyInvalidatedException) {
|
||||
return true;
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+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 java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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 com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
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;
|
||||
|
||||
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,21 @@
|
||||
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.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());
|
||||
} 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,24 +1,27 @@
|
||||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
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);
|
||||
return new String(encoded, StandardCharsets.UTF_8);
|
||||
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,21 @@
|
||||
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;
|
||||
|
||||
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());
|
||||
} 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,22 +9,10 @@ public class EditTextHelper {
|
||||
private EditTextHelper() {
|
||||
}
|
||||
|
||||
public static void clearEditText(EditText text) {
|
||||
text.getText().clear();
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text) {
|
||||
return getEditTextChars(text, false);
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text, boolean removeSpaces) {
|
||||
String editTextString = text.getText().toString();
|
||||
if (removeSpaces) {
|
||||
editTextString = editTextString.replaceAll("\\s","");
|
||||
}
|
||||
|
||||
char[] chars = new char[editTextString.length()];
|
||||
editTextString.getChars(0, editTextString.length(), chars, 0);
|
||||
Editable editable = text.getText();
|
||||
char[] chars = new char[editable.length()];
|
||||
editable.getChars(0, editable.length(), chars, 0);
|
||||
return chars;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.View;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
|
||||
public class FabScrollHelper {
|
||||
private View _fabMenu;
|
||||
@@ -18,6 +17,26 @@ public class FabScrollHelper {
|
||||
|
||||
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;
|
||||
@@ -32,18 +51,7 @@ public class FabScrollHelper {
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
|
||||
_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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.mattprecious.swirl.SwirlView;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -3,8 +3,13 @@ package com.beemdevelopment.aegis.helpers;
|
||||
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,31 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class AccountNameComparator implements Comparator<DatabaseEntry> {
|
||||
public class AccountNameComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(DatabaseEntry a, DatabaseEntry b) {
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return a.getName().compareToIgnoreCase(b.getName());
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class IssuerNameComparator implements Comparator<DatabaseEntry> {
|
||||
public class IssuerNameComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(DatabaseEntry a, DatabaseEntry b) {
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
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) {
|
||||
return getIssuers().stream()
|
||||
.anyMatch(is -> is.toLowerCase().contains(issuer.toLowerCase()));
|
||||
}
|
||||
|
||||
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,52 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
|
||||
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()) {
|
||||
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,20 +1,30 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFile;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileException;
|
||||
import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
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 {
|
||||
|
||||
@@ -23,32 +33,28 @@ public class AegisImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = reader.readAll();
|
||||
DatabaseFile file = DatabaseFile.fromBytes(bytes);
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
VaultFile file = VaultFile.fromBytes(bytes);
|
||||
if (file.isEncrypted()) {
|
||||
return new EncryptedState(file);
|
||||
}
|
||||
return new DecryptedState(file.getContent());
|
||||
} catch (DatabaseFileException | IOException e) {
|
||||
} catch (VaultFileException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EncryptedState extends State {
|
||||
private DatabaseFile _file;
|
||||
private VaultFile _file;
|
||||
|
||||
private EncryptedState(DatabaseFile file) {
|
||||
private EncryptedState(VaultFile file) {
|
||||
super(true);
|
||||
_file = file;
|
||||
}
|
||||
@@ -57,14 +63,46 @@ public class AegisImporter extends DatabaseImporter {
|
||||
return _file.getHeader().getSlots();
|
||||
}
|
||||
|
||||
public State decrypt(DatabaseFileCredentials creds) throws DatabaseFileException {
|
||||
JSONObject obj = _file.getContent(creds);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +123,7 @@ public class AegisImporter extends DatabaseImporter {
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject entryObj = array.getJSONObject(i);
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(entryObj);
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
@@ -98,10 +136,10 @@ public class AegisImporter extends DatabaseImporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
return DatabaseEntry.fromJson(obj);
|
||||
} catch (JSONException | OtpInfoException | Base64Exception e) {
|
||||
return VaultEntry.fromJson(obj);
|
||||
} catch (VaultEntryException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,41 @@ 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.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
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;
|
||||
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 javax.crypto.BadPaddingException;
|
||||
@@ -32,28 +44,31 @@ 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 String getAppPkgName() {
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = reader.readAll();
|
||||
bytes = IOUtils.readAll(stream);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
@@ -80,24 +95,15 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
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 {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support encrypted andOTP backups
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
// extract nonce and tag
|
||||
byte[] nonce = Arrays.copyOfRange(_data, 0, CryptoUtils.CRYPTO_AEAD_NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - CryptoUtils.CRYPTO_AEAD_TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
|
||||
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
|
||||
int offset = CryptoUtils.CRYPTO_AEAD_NONCE_SIZE;
|
||||
int len = _data.length - CryptoUtils.CRYPTO_AEAD_NONCE_SIZE - CryptoUtils.CRYPTO_AEAD_TAG_SIZE;
|
||||
CryptResult result = CryptoUtils.decrypt(_data, offset, len, cipher, params);
|
||||
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);
|
||||
@@ -110,15 +116,83 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try {
|
||||
DecryptedState state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +211,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
try {
|
||||
JSONObject obj = _obj.getJSONObject(i);
|
||||
DatabaseEntry entry = convertEntry(obj);
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
@@ -149,12 +223,12 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
String type = obj.getString("type").toLowerCase();
|
||||
String algo = obj.getString("algorithm");
|
||||
int digits = obj.getInt("digits");
|
||||
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
|
||||
OtpInfo info;
|
||||
switch (type) {
|
||||
@@ -165,7 +239,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
|
||||
break;
|
||||
case "steam":
|
||||
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
|
||||
info = new SteamInfo(secret, algo, digits, obj.optInt("period", TotpInfo.DEFAULT_PERIOD));
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterException("unsupported otp type: " + type);
|
||||
@@ -174,18 +248,85 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
String name;
|
||||
String issuer = "";
|
||||
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
if (obj.has("issuer")) {
|
||||
name = obj.getString("label");
|
||||
issuer = obj.getString("issuer");
|
||||
} else {
|
||||
name = parts[0];
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
} else {
|
||||
name = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return new DatabaseEntry(info, name, issuer);
|
||||
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2,44 +2,44 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class DatabaseImporter {
|
||||
private Context _context;
|
||||
|
||||
private static Map<String, Class<? extends DatabaseImporter>> _importers;
|
||||
private static Map<String, Class<? extends DatabaseImporter>> _appImporters;
|
||||
private static List<Definition> _importers;
|
||||
|
||||
static {
|
||||
// note: keep these lists sorted alphabetically
|
||||
_importers = new LinkedHashMap<>();
|
||||
_importers.put("Aegis", AegisImporter.class);
|
||||
_importers.put("andOTP", AndOtpImporter.class);
|
||||
_importers.put("FreeOTP", FreeOtpImporter.class);
|
||||
_importers.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_importers.put("Steam", SteamImporter.class);
|
||||
|
||||
_appImporters = new LinkedHashMap<>();
|
||||
_appImporters.put("FreeOTP", FreeOtpImporter.class);
|
||||
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_appImporters.put("Steam", SteamImporter.class);
|
||||
_importers = new ArrayList<>();
|
||||
_importers.add(new Definition("2FAS Authenticator", TwoFASImporter.class, R.string.importer_help_2fas, false));
|
||||
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
|
||||
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
|
||||
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
|
||||
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
|
||||
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
|
||||
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
|
||||
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
|
||||
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
|
||||
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
|
||||
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
|
||||
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
|
||||
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
|
||||
}
|
||||
|
||||
public DatabaseImporter(Context context) {
|
||||
@@ -50,20 +50,27 @@ public abstract class DatabaseImporter {
|
||||
return _context;
|
||||
}
|
||||
|
||||
public SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
return getAppPath(getAppPkgName(), getAppSubPath());
|
||||
}
|
||||
protected abstract SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
|
||||
|
||||
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
|
||||
PackageManager man = getContext().getPackageManager();
|
||||
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
|
||||
}
|
||||
|
||||
protected abstract String getAppPkgName();
|
||||
protected abstract State read(InputStream stream, boolean isInternal) throws DatabaseImporterException;
|
||||
|
||||
protected abstract String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
|
||||
public State read(InputStream stream) throws DatabaseImporterException {
|
||||
return read(stream, false);
|
||||
}
|
||||
|
||||
public abstract State read(FileReader reader) throws DatabaseImporterException;
|
||||
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile file = getAppPath();
|
||||
try (SuFileInputStream stream = new SuFileInputStream(file)) {
|
||||
return read(stream, true);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static DatabaseImporter create(Context context, Class<? extends DatabaseImporter> type) {
|
||||
try {
|
||||
@@ -74,12 +81,42 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
|
||||
return Collections.unmodifiableMap(_importers);
|
||||
public static List<Definition> getImporters(boolean isDirect) {
|
||||
if (isDirect) {
|
||||
return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(_importers);
|
||||
}
|
||||
|
||||
public static Map<String, Class<? extends DatabaseImporter>> getAppImporters() {
|
||||
return Collections.unmodifiableMap(_appImporters);
|
||||
public static class Definition {
|
||||
private final String _name;
|
||||
private final Class<? extends DatabaseImporter> _type;
|
||||
private final @StringRes int _help;
|
||||
private final boolean _supportsDirect;
|
||||
|
||||
public Definition(String name, Class<? extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
|
||||
_name = name;
|
||||
_type = type;
|
||||
_help = help;
|
||||
_supportsDirect = supportsDirect;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public Class<? extends DatabaseImporter> getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public @StringRes int getHelp() {
|
||||
return _help;
|
||||
}
|
||||
|
||||
public boolean supportsDirect() {
|
||||
return _supportsDirect;
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class State {
|
||||
@@ -111,10 +148,10 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private List<DatabaseEntry> _entries = new ArrayList<>();
|
||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
public void addEntry(VaultEntry entry) {
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
@@ -122,7 +159,7 @@ public abstract class DatabaseImporter {
|
||||
_errors.add(error);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
public UUIDMap<VaultEntry> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
@@ -131,48 +168,9 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileReader implements Closeable {
|
||||
private InputStream _stream;
|
||||
|
||||
private FileReader(InputStream stream) {
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
public static FileReader open(String filename)
|
||||
throws FileNotFoundException {
|
||||
FileInputStream stream = new FileInputStream(filename);
|
||||
return new FileReader(stream);
|
||||
}
|
||||
|
||||
public static FileReader open(SuFile file)
|
||||
throws FileNotFoundException {
|
||||
SuFileInputStream stream = new SuFileInputStream(file);
|
||||
return new FileReader(stream);
|
||||
}
|
||||
|
||||
public static FileReader open(Context context, Uri uri)
|
||||
throws FileNotFoundException {
|
||||
InputStream stream = context.getContentResolver().openInputStream(uri);
|
||||
return new FileReader(stream);
|
||||
}
|
||||
|
||||
public byte[] readAll() throws IOException {
|
||||
ByteInputStream stream = ByteInputStream.create(_stream);
|
||||
return stream.getBytes();
|
||||
}
|
||||
|
||||
public InputStream getStream() {
|
||||
return _stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
_stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
public interface DecryptListener {
|
||||
void onStateDecrypted(State state);
|
||||
void onError(Exception e);
|
||||
public static abstract class DecryptListener {
|
||||
protected abstract void onStateDecrypted(State state);
|
||||
protected abstract void onError(Exception e);
|
||||
protected abstract void onCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.importers;
|
||||
public class DatabaseImporterEntryException extends Exception {
|
||||
private String _text;
|
||||
|
||||
public DatabaseImporterEntryException(String message, String text) {
|
||||
super(message);
|
||||
_text = text;
|
||||
}
|
||||
|
||||
public DatabaseImporterEntryException(Throwable cause, String text) {
|
||||
super(cause);
|
||||
_text = text;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
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.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -16,6 +20,7 @@ import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -28,33 +33,34 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
List<XmlEntry> entries = parse(parser);
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (!entry.Name.equals("tokenOrder")) {
|
||||
entries.add(new JSONObject(entry.Value));
|
||||
}
|
||||
}
|
||||
return new State(entries);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
} catch (XmlPullParserException | IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private List<XmlEntry> _entries;
|
||||
private List<JSONObject> _entries;
|
||||
|
||||
private State(List<XmlEntry> entries) {
|
||||
public State(List<JSONObject> entries) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
}
|
||||
@@ -63,34 +69,37 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (XmlEntry xmlEntry : _entries) {
|
||||
// TODO: order
|
||||
if (!xmlEntry.Name.equals("tokenOrder")) {
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(xmlEntry);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
for (JSONObject obj : _entries) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(XmlEntry entry) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
JSONObject obj = new JSONObject(entry.Value);
|
||||
|
||||
String type = obj.getString("type").toLowerCase();
|
||||
String algo = obj.getString("algo");
|
||||
int digits = obj.getInt("digits");
|
||||
byte[] secret = toBytes(obj.getJSONArray("secret"));
|
||||
|
||||
String issuer = obj.getString("issuerExt");
|
||||
String name = obj.optString("label");
|
||||
|
||||
OtpInfo info;
|
||||
switch (type) {
|
||||
case "totp":
|
||||
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
|
||||
int period = obj.getInt("period");
|
||||
if (issuer.equals("Steam")) {
|
||||
info = new SteamInfo(secret, algo, digits, period);
|
||||
} else {
|
||||
info = new TotpInfo(secret, algo, digits, period);
|
||||
}
|
||||
break;
|
||||
case "hotp":
|
||||
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
|
||||
@@ -99,36 +108,13 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
throw new DatabaseImporterException("unsupported otp type: " + type);
|
||||
}
|
||||
|
||||
String issuer = obj.getString("issuerExt");
|
||||
String name = obj.optString("label");
|
||||
return new DatabaseEntry(info, name, issuer);
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.Value);
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<XmlEntry> parse(XmlPullParser parser)
|
||||
throws IOException, XmlPullParserException {
|
||||
List<XmlEntry> entries = new ArrayList<>();
|
||||
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parser.getName().equals("string")) {
|
||||
skip(parser);
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.add(parseEntry(parser));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static byte[] toBytes(JSONArray array) throws JSONException {
|
||||
byte[] bytes = new byte[array.length()];
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
@@ -136,49 +122,4 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
String name = parser.getAttributeValue(null, "name");
|
||||
String value = parseText(parser);
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
|
||||
XmlEntry entry = new XmlEntry();
|
||||
entry.Name = name;
|
||||
entry.Value = value;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
String text = "";
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
text = parser.getText();
|
||||
parser.nextTag();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
// source: https://developer.android.com/training/basics/network-ops/xml.html
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
int depth = 1;
|
||||
while (depth != 0) {
|
||||
switch (parser.next()) {
|
||||
case XmlPullParser.END_TAG:
|
||||
depth--;
|
||||
break;
|
||||
case XmlPullParser.START_TAG:
|
||||
depth++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class XmlEntry {
|
||||
String Name;
|
||||
String Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
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.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FreeOtpPlusImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/tokens.xml";
|
||||
private static final String _pkgName = "org.liberty.android.freeotpplus";
|
||||
|
||||
public FreeOtpPlusImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
State state;
|
||||
|
||||
if (isInternal) {
|
||||
state = new FreeOtpImporter(getContext()).read(stream);
|
||||
} else {
|
||||
try {
|
||||
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
|
||||
JSONObject obj = new JSONObject(json);
|
||||
JSONArray array = obj.getJSONArray("tokens");
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
entries.add(array.getJSONObject(i));
|
||||
}
|
||||
|
||||
state = new FreeOtpImporter.State(entries);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,21 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
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.topjohnwu.superuser.ShellUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
|
||||
|
||||
public class GoogleAuthImporter extends DatabaseImporter {
|
||||
private static final int TYPE_TOTP = 0;
|
||||
private static final int TYPE_HOTP = 1;
|
||||
@@ -34,47 +28,23 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
File file;
|
||||
|
||||
try {
|
||||
// create a temporary copy of the database so that SQLiteDatabase can open it
|
||||
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
ShellUtils.pump(reader.getStream(), out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Entry entry = new Entry(cursor);
|
||||
entries.add(entry);
|
||||
} while(cursor.moveToNext());
|
||||
}
|
||||
|
||||
return new State(entries);
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
file.delete();
|
||||
}
|
||||
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, path, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
@@ -91,7 +61,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
|
||||
for (Entry sqlEntry : _entries) {
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(sqlEntry);
|
||||
VaultEntry entry = convertEntry(sqlEntry);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
@@ -101,9 +71,9 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
|
||||
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
|
||||
|
||||
OtpInfo info;
|
||||
switch (entry.getType()) {
|
||||
@@ -123,34 +93,14 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
name = parts[1];
|
||||
}
|
||||
|
||||
return new DatabaseEntry(info, name, entry.getIssuer());
|
||||
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
|
||||
return new VaultEntry(info, name, entry.getIssuer());
|
||||
} catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getString(Cursor cursor, String columnName) {
|
||||
return getString(cursor, columnName, null);
|
||||
}
|
||||
|
||||
private static String getString(Cursor cursor, String columnName, String def) {
|
||||
String res = cursor.getString(cursor.getColumnIndex(columnName));
|
||||
if (res == null) {
|
||||
return def;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private static int getInt(Cursor cursor, String columnName) {
|
||||
return cursor.getInt(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
private static long getLong(Cursor cursor, String columnName) {
|
||||
return cursor.getLong(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
private static class Entry {
|
||||
private static class Entry extends SqlImporterHelper.Entry {
|
||||
private int _type;
|
||||
private String _secret;
|
||||
private String _email;
|
||||
@@ -158,11 +108,12 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
private long _counter;
|
||||
|
||||
public Entry(Cursor cursor) {
|
||||
_type = getInt(cursor, "type");
|
||||
_secret = getString(cursor, "secret");
|
||||
_email = getString(cursor, "email", "");
|
||||
_issuer = getString(cursor, "issuer", "");
|
||||
_counter = getLong(cursor, "counter");
|
||||
super(cursor);
|
||||
_type = SqlImporterHelper.getInt(cursor, "type");
|
||||
_secret = SqlImporterHelper.getString(cursor, "secret");
|
||||
_email = SqlImporterHelper.getString(cursor, "email", "");
|
||||
_issuer = SqlImporterHelper.getString(cursor, "issuer", "");
|
||||
_counter = SqlImporterHelper.getLong(cursor, "counter");
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GoogleAuthUriImporter extends DatabaseImporter {
|
||||
public GoogleAuthUriImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
|
||||
try (InputStreamReader streamReader = new InputStreamReader(stream);
|
||||
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (!line.isEmpty()) {
|
||||
lines.add(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new GoogleAuthUriImporter.State(lines);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private ArrayList<String> _lines;
|
||||
|
||||
private State(ArrayList<String> lines) {
|
||||
super(false);
|
||||
_lines = lines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseImporter.Result convert() {
|
||||
DatabaseImporter.Result result = new DatabaseImporter.Result();
|
||||
|
||||
for (String line : _lines) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(line);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
|
||||
return new VaultEntry(info);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class MicrosoftAuthImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "databases/PhoneFactor";
|
||||
private static final String _pkgName = "com.azure.authenticator";
|
||||
|
||||
private static final int TYPE_TOTP = 0;
|
||||
private static final int TYPE_MICROSOFT = 1;
|
||||
|
||||
public MicrosoftAuthImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, path, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private List<Entry> _entries;
|
||||
|
||||
private State(List<Entry> entries) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (Entry sqlEntry : _entries) {
|
||||
try {
|
||||
int type = sqlEntry.getType();
|
||||
if (type == TYPE_TOTP || type == TYPE_MICROSOFT) {
|
||||
VaultEntry entry = convertEntry(sqlEntry);
|
||||
result.addEntry(entry);
|
||||
}
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret;
|
||||
int digits = 6;
|
||||
|
||||
switch (entry.getType()) {
|
||||
case TYPE_TOTP:
|
||||
secret = Base32.decode(entry.getSecret());
|
||||
break;
|
||||
case TYPE_MICROSOFT:
|
||||
digits = 8;
|
||||
secret = Base64.decode(entry.getSecret());
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
|
||||
}
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, TotpInfo.DEFAULT_PERIOD);
|
||||
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
|
||||
} catch (EncodingException | OtpInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Entry extends SqlImporterHelper.Entry {
|
||||
private int _type;
|
||||
private String _secret;
|
||||
private String _issuer;
|
||||
private String _userName;
|
||||
|
||||
public Entry(Cursor cursor) {
|
||||
super(cursor);
|
||||
_type = SqlImporterHelper.getInt(cursor, "account_type");
|
||||
_secret = SqlImporterHelper.getString(cursor, "oath_secret_key");
|
||||
_issuer = SqlImporterHelper.getString(cursor, "name");
|
||||
_userName = SqlImporterHelper.getString(cursor, "username");
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public String getSecret() {
|
||||
return _secret;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return _userName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.google.common.io.Files;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
|
||||
|
||||
public class SqlImporterHelper {
|
||||
private Context _context;
|
||||
|
||||
public SqlImporterHelper(Context context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public <T extends Entry> List<T> read(Class<T> type, SuFile path, String table) throws DatabaseImporterException {
|
||||
File dir = Files.createTempDir();
|
||||
File mainFile = new File(dir, path.getName());
|
||||
|
||||
List<File> fileCopies = new ArrayList<>();
|
||||
for (SuFile file : SqlImporterHelper.findDatabaseFiles(path)) {
|
||||
// create temporary copies of the database files so that SQLiteDatabase can open them
|
||||
File fileCopy = null;
|
||||
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
|
||||
fileCopy = new File(dir, file.getName());
|
||||
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
|
||||
IOUtils.copy(inStream, out);
|
||||
}
|
||||
fileCopies.add(fileCopy);
|
||||
} catch (IOException e) {
|
||||
if (fileCopy != null) {
|
||||
fileCopy.delete();
|
||||
}
|
||||
|
||||
for (File fileCopy2 : fileCopies) {
|
||||
fileCopy2.delete();
|
||||
}
|
||||
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return read(type, mainFile, table);
|
||||
} finally {
|
||||
for (File fileCopy : fileCopies) {
|
||||
fileCopy.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SuFile[] findDatabaseFiles(SuFile path) throws DatabaseImporterException {
|
||||
SuFile[] files = path.getParentFile().listFiles((d, name) -> name.startsWith(path.getName()));
|
||||
if (files == null || files.length == 0) {
|
||||
throw new DatabaseImporterException(String.format("File does not exist: %s", path.getAbsolutePath()));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
|
||||
File file = null;
|
||||
try {
|
||||
// create a temporary copy of the database so that SQLiteDatabase can open it
|
||||
file = File.createTempFile("db-import-", "", _context.getCacheDir());
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
IOUtils.copy(inStream, out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (file != null) {
|
||||
file.delete();
|
||||
}
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
return read(type, file, table);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Entry> List<T> read(Class<T> type, File file, String table) throws DatabaseImporterException {
|
||||
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
|
||||
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
|
||||
List<T> entries = new ArrayList<>();
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
T entry = type.getDeclaredConstructor(Cursor.class).newInstance(cursor);
|
||||
entries.add(entry);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
|
||||
return entries;
|
||||
} catch (InstantiationException | IllegalAccessException
|
||||
| NoSuchMethodException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
public static String getString(Cursor cursor, String columnName) {
|
||||
return cursor.getString(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
public static String getString(Cursor cursor, String columnName, String def) {
|
||||
String res = cursor.getString(cursor.getColumnIndex(columnName));
|
||||
if (res == null) {
|
||||
return def;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
public static int getInt(Cursor cursor, String columnName) {
|
||||
return cursor.getInt(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
public static long getLong(Cursor cursor, String columnName) {
|
||||
return cursor.getLong(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
public static abstract class Entry {
|
||||
public Entry(Cursor cursor) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,19 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SteamImporter extends DatabaseImporter {
|
||||
@@ -26,26 +27,23 @@ public class SteamImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
// NOTE: this assumes that a global root shell has already been obtained by the caller
|
||||
SuFile path = getAppPath(getAppPkgName(), _subDir);
|
||||
SuFile path = getAppPath(_pkgName, _subDir);
|
||||
SuFile[] files = path.listFiles((d, name) -> name.startsWith("Steamguard-"));
|
||||
if (files == null || files.length == 0) {
|
||||
throw new DatabaseImporterException(String.format("Empty directory: %s", path.getAbsolutePath()));
|
||||
}
|
||||
|
||||
// TODO: handle multiple files (can this even occur?)
|
||||
return new SuFile(_subDir, files[0].getName()).getPath();
|
||||
return files[0];
|
||||
}
|
||||
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
|
||||
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
return new State(obj);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
@@ -65,7 +63,7 @@ public class SteamImporter extends DatabaseImporter {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
DatabaseEntry entry = convertEntry(_obj);
|
||||
VaultEntry entry = convertEntry(_obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
@@ -74,14 +72,14 @@ public class SteamImporter extends DatabaseImporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base64.decode(obj.getString("shared_secret"));
|
||||
SteamInfo info = new SteamInfo(secret);
|
||||
|
||||
String account = obj.getString("account_name");
|
||||
return new DatabaseEntry(info, account, "Steam");
|
||||
} catch (JSONException | Base64Exception | OtpInfoException e) {
|
||||
return new VaultEntry(info, account, "Steam");
|
||||
} catch (JSONException | EncodingException | OtpInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
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.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
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 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.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
|
||||
private static final String _pkgName = "com.authenticator.authservice2";
|
||||
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
|
||||
private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
|
||||
private static final byte[] IV = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
|
||||
|
||||
public TotpAuthenticatorImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
if (isInternal) {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
String data = null;
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals(PREF_KEY)) {
|
||||
data = entry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
|
||||
}
|
||||
|
||||
List<JSONObject> entries = parse(data);
|
||||
return new DecryptedState(entries);
|
||||
} else {
|
||||
byte[] base64 = IOUtils.readAll(stream);
|
||||
byte[] cipherText = Base64.decode(base64);
|
||||
return new EncryptedState(cipherText);
|
||||
}
|
||||
} catch (IOException | XmlPullParserException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<JSONObject> parse(String data) throws JSONException {
|
||||
JSONArray array = new JSONArray(data);
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); ++i) {
|
||||
JSONObject obj = array.getJSONObject(i);
|
||||
entries.add(obj);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private byte[] _data;
|
||||
|
||||
public EncryptedState(byte[] data) {
|
||||
super(true);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
try {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this is not a secure way to derive a key from a password
|
||||
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec spec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
|
||||
byte[] bytes = cipher.doFinal(_data);
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
JSONArray keys = obj.names();
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
if (keys != null && keys.length() > 0) {
|
||||
entries = parse((String) keys.get(0));
|
||||
}
|
||||
|
||||
return new DecryptedState(entries);
|
||||
} catch (NoSuchAlgorithmException
|
||||
| NoSuchPaddingException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException
|
||||
| JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setMessage(R.string.choose_totpauth_importer)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
decrypt(password, listener);
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
})
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> {
|
||||
decrypt(PASSWORD, listener);
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private void decrypt(char[] password, DecryptListener listener) {
|
||||
try {
|
||||
DecryptedState state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends DatabaseImporter.State {
|
||||
private List<JSONObject> _objs;
|
||||
|
||||
private DecryptedState(List<JSONObject> objs) {
|
||||
super(false);
|
||||
_objs = objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (JSONObject obj : _objs) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
int base = obj.getInt("base");
|
||||
String secretString = obj.getString("key");
|
||||
|
||||
byte[] secret;
|
||||
switch (base) {
|
||||
case 16:
|
||||
secret = Hex.decode(secretString);
|
||||
break;
|
||||
case 32:
|
||||
secret = Base32.decode(secretString);
|
||||
break;
|
||||
case 64:
|
||||
secret = Base64.decode(secretString);
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
|
||||
}
|
||||
|
||||
TotpInfo info = new TotpInfo(secret);
|
||||
String name = obj.optString("name");
|
||||
String issuer = obj.optString("issuer");
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
} catch (JSONException | OtpInfoException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
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.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TwoFASImporter extends DatabaseImporter {
|
||||
public TwoFASImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
|
||||
JSONObject obj = new JSONObject(json);
|
||||
int version = obj.getInt("schemaVersion");
|
||||
if (version > 1) {
|
||||
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
|
||||
}
|
||||
|
||||
JSONArray array = obj.getJSONArray("services");
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
entries.add(array.getJSONObject(i));
|
||||
}
|
||||
|
||||
return new TwoFASImporter.State(entries);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private final List<JSONObject> _entries;
|
||||
|
||||
public State(List<JSONObject> entries) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (JSONObject obj : _entries) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
JSONObject info = obj.getJSONObject("otp");
|
||||
String issuer = info.getString("issuer");
|
||||
String name = info.optString("account");
|
||||
|
||||
OtpInfo otp = new TotpInfo(secret);
|
||||
return new VaultEntry(otp, name, issuer);
|
||||
} catch (OtpInfoException | JSONException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class WinAuthImporter extends DatabaseImporter {
|
||||
public WinAuthImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
|
||||
DatabaseImporter.State state = importer.read(stream);
|
||||
return new State(state);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private DatabaseImporter.State _state;
|
||||
|
||||
private State(DatabaseImporter.State state) {
|
||||
super(false);
|
||||
_state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = _state.convert();
|
||||
|
||||
for (VaultEntry entry : result.getEntries()) {
|
||||
entry.setIssuer(entry.getName());
|
||||
entry.setName("WinAuth");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class GlideLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Glide License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.glide_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.glide_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://github.com/bumptech/glide/blob/master/LICENSE";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class ProtobufLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Protocol Buffers License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,20 @@ package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.GoogleAuthProtos;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GoogleAuthInfo implements Serializable {
|
||||
public static final String SCHEME = "otpauth";
|
||||
public static final String SCHEME_EXPORT = "otpauth-migration";
|
||||
|
||||
public class GoogleAuthInfo {
|
||||
private OtpInfo _info;
|
||||
private String _accountName;
|
||||
private String _issuer;
|
||||
@@ -22,21 +32,25 @@ public class GoogleAuthInfo {
|
||||
|
||||
public Uri getUri() {
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme("otpauth");
|
||||
builder.scheme(SCHEME);
|
||||
|
||||
if (_info instanceof TotpInfo) {
|
||||
builder.authority("totp");
|
||||
if (_info instanceof SteamInfo) {
|
||||
builder.authority("steam");
|
||||
} else {
|
||||
builder.authority("totp");
|
||||
}
|
||||
builder.appendQueryParameter("period", Integer.toString(((TotpInfo)_info).getPeriod()));
|
||||
} else if (_info instanceof HotpInfo) {
|
||||
builder.authority("hotp");
|
||||
builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter()));
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
|
||||
}
|
||||
|
||||
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
|
||||
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
|
||||
builder.appendQueryParameter("secret", new String(Base32.encode(_info.getSecret())));
|
||||
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
|
||||
|
||||
if (_issuer != null && !_issuer.equals("")) {
|
||||
builder.path(String.format("%s:%s", _issuer, _accountName));
|
||||
@@ -51,35 +65,37 @@ public class GoogleAuthInfo {
|
||||
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("bad uri format");
|
||||
throw new GoogleAuthInfoException(uri, String.format("Bad URI format: %s", s));
|
||||
}
|
||||
return GoogleAuthInfo.parseUri(uri);
|
||||
}
|
||||
|
||||
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals("otpauth")) {
|
||||
throw new GoogleAuthInfoException("unsupported protocol");
|
||||
if (scheme == null || !scheme.equals(SCHEME)) {
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
|
||||
}
|
||||
|
||||
// 'secret' is a required parameter
|
||||
String encodedSecret = uri.getQueryParameter("secret");
|
||||
if (encodedSecret == null) {
|
||||
throw new GoogleAuthInfoException("'secret' is not set");
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'secret' is not present");
|
||||
}
|
||||
|
||||
// decode secret
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(encodedSecret.toCharArray());
|
||||
} catch (Base32Exception e) {
|
||||
throw new GoogleAuthInfoException("bad secret", e);
|
||||
secret = parseSecret(encodedSecret);
|
||||
} catch (EncodingException e) {
|
||||
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
||||
}
|
||||
|
||||
// check the otp type
|
||||
OtpInfo info;
|
||||
try {
|
||||
String type = uri.getHost();
|
||||
if (type == null) {
|
||||
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "totp":
|
||||
TotpInfo totpInfo = new TotpInfo(secret);
|
||||
@@ -89,20 +105,28 @@ public class GoogleAuthInfo {
|
||||
}
|
||||
info = totpInfo;
|
||||
break;
|
||||
case "steam":
|
||||
SteamInfo steamInfo = new SteamInfo(secret);
|
||||
period = uri.getQueryParameter("period");
|
||||
if (period != null) {
|
||||
steamInfo.setPeriod(Integer.parseInt(period));
|
||||
}
|
||||
info = steamInfo;
|
||||
break;
|
||||
case "hotp":
|
||||
HotpInfo hotpInfo = new HotpInfo(secret);
|
||||
String counter = uri.getQueryParameter("counter");
|
||||
if (counter == null) {
|
||||
throw new GoogleAuthInfoException("'counter' was not set");
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'counter' is not present");
|
||||
}
|
||||
hotpInfo.setCounter(Long.parseLong(counter));
|
||||
info = hotpInfo;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
} catch (OtpInfoException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
// provider info used to disambiguate accounts
|
||||
@@ -141,13 +165,120 @@ public class GoogleAuthInfo {
|
||||
if (digits != null) {
|
||||
info.setDigits(Integer.parseInt(digits));
|
||||
}
|
||||
} catch (OtpInfoException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
return new GoogleAuthInfo(info, accountName, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given base 32 secret, while being tolerant of whitespace and dashes.
|
||||
*/
|
||||
public static byte[] parseSecret(String s) throws EncodingException {
|
||||
s = s.trim().replace("-", "").replace(" ", "");
|
||||
return Base32.decode(s);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException(uri, "Bad URI format");
|
||||
}
|
||||
return GoogleAuthInfo.parseExportUri(uri);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
|
||||
throw new GoogleAuthInfoException(uri, "Unsupported protocol");
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null || !host.equals("offline")) {
|
||||
throw new GoogleAuthInfoException(uri, "Unsupported host");
|
||||
}
|
||||
|
||||
String data = uri.getQueryParameter("data");
|
||||
if (data == null) {
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'data' is not set");
|
||||
}
|
||||
|
||||
GoogleAuthProtos.MigrationPayload payload;
|
||||
try {
|
||||
byte[] bytes = Base64.decode(data);
|
||||
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
|
||||
} catch (EncodingException | InvalidProtocolBufferException e) {
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
List<GoogleAuthInfo> infos = new ArrayList<>();
|
||||
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
|
||||
OtpInfo otp;
|
||||
try {
|
||||
int digits;
|
||||
switch (params.getDigits()) {
|
||||
case DIGIT_COUNT_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case DIGIT_COUNT_SIX:
|
||||
digits = TotpInfo.DEFAULT_DIGITS;
|
||||
break;
|
||||
case DIGIT_COUNT_EIGHT:
|
||||
digits = 8;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported digits: %d", params.getDigits().ordinal()));
|
||||
}
|
||||
|
||||
String algo;
|
||||
switch (params.getAlgorithm()) {
|
||||
case ALGORITHM_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case ALGORITHM_SHA1:
|
||||
algo = "SHA1";
|
||||
break;
|
||||
case ALGORITHM_SHA256:
|
||||
algo = "SHA256";
|
||||
break;
|
||||
case ALGORITHM_SHA512:
|
||||
algo = "SHA512";
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
|
||||
}
|
||||
|
||||
byte[] secret = params.getSecret().toByteArray();
|
||||
switch (params.getType()) {
|
||||
case OTP_TYPE_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case OTP_TYPE_TOTP:
|
||||
otp = new TotpInfo(secret, algo, digits, TotpInfo.DEFAULT_PERIOD);
|
||||
break;
|
||||
case OTP_TYPE_HOTP:
|
||||
otp = new HotpInfo(secret, algo, digits, params.getCounter());
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||
}
|
||||
} catch (OtpInfoException e){
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
String name = params.getName();
|
||||
String issuer = params.getIssuer();
|
||||
int colonI = name.indexOf(':');
|
||||
if (issuer.isEmpty() && colonI != -1) {
|
||||
issuer = name.substring(0, colonI);
|
||||
name = name.substring(colonI + 1);
|
||||
}
|
||||
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(otp, name, issuer);
|
||||
infos.add(info);
|
||||
}
|
||||
|
||||
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
@@ -155,4 +286,34 @@ public class GoogleAuthInfo {
|
||||
public String getAccountName() {
|
||||
return _accountName;
|
||||
}
|
||||
|
||||
public static class Export {
|
||||
private int _batchId;
|
||||
private int _batchIndex;
|
||||
private int _batchSize;
|
||||
private List<GoogleAuthInfo> _entries;
|
||||
|
||||
public Export(List<GoogleAuthInfo> entries, int batchId, int batchIndex, int batchSize) {
|
||||
_batchId = batchId;
|
||||
_batchIndex = batchIndex;
|
||||
_batchSize = batchSize;
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
public List<GoogleAuthInfo> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
public int getBatchSize() {
|
||||
return _batchSize;
|
||||
}
|
||||
|
||||
public int getBatchIndex() {
|
||||
return _batchIndex;
|
||||
}
|
||||
|
||||
public int getBatchId() {
|
||||
return _batchId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public class GoogleAuthInfoException extends Exception {
|
||||
public GoogleAuthInfoException(Throwable cause) {
|
||||
private final Uri _uri;
|
||||
|
||||
public GoogleAuthInfoException(Uri uri, Throwable cause) {
|
||||
super(cause);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public GoogleAuthInfoException(String message) {
|
||||
public GoogleAuthInfoException(Uri uri, String message) {
|
||||
super(message);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public GoogleAuthInfoException(String message, Throwable cause) {
|
||||
public GoogleAuthInfoException(Uri uri, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the scheme of the URI is phonefactor://.
|
||||
*/
|
||||
public boolean isPhoneFactor() {
|
||||
return _uri != null && _uri.getScheme() != null && _uri.getScheme().equals("phonefactor");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -19,6 +33,7 @@ public class GoogleAuthInfoException extends Exception {
|
||||
if (cause == null) {
|
||||
return super.getMessage();
|
||||
}
|
||||
|
||||
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class HotpInfo extends OtpInfo {
|
||||
public static final String ID = "hotp";
|
||||
public static final int DEFAULT_COUNTER = 0;
|
||||
|
||||
private long _counter;
|
||||
|
||||
@@ -20,7 +21,7 @@ public class HotpInfo extends OtpInfo {
|
||||
}
|
||||
|
||||
public HotpInfo(byte[] secret) throws OtpInfoException {
|
||||
this(secret, 0);
|
||||
this(secret, DEFAULT_COUNTER);
|
||||
}
|
||||
|
||||
public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
|
||||
@@ -34,12 +35,12 @@ public class HotpInfo extends OtpInfo {
|
||||
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
|
||||
return otp.toString();
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
public String getTypeId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base32Exception;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -10,12 +10,15 @@ import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
|
||||
public abstract class OtpInfo implements Serializable {
|
||||
public static final int DEFAULT_DIGITS = 6;
|
||||
public static final String DEFAULT_ALGORITHM = "SHA1";
|
||||
|
||||
private byte[] _secret;
|
||||
private String _algorithm;
|
||||
private int _digits;
|
||||
|
||||
public OtpInfo(byte[] secret) throws OtpInfoException {
|
||||
this(secret, "SHA1", 6);
|
||||
this(secret, DEFAULT_ALGORITHM, DEFAULT_DIGITS);
|
||||
}
|
||||
|
||||
public OtpInfo(byte[] secret, String algorithm, int digits) throws OtpInfoException {
|
||||
@@ -26,7 +29,11 @@ public abstract class OtpInfo implements Serializable {
|
||||
|
||||
public abstract String getOtp();
|
||||
|
||||
public abstract String getType();
|
||||
public abstract String getTypeId();
|
||||
|
||||
public String getType() {
|
||||
return getTypeId().toUpperCase();
|
||||
}
|
||||
|
||||
public JSONObject toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
@@ -93,7 +100,7 @@ public abstract class OtpInfo implements Serializable {
|
||||
OtpInfo info;
|
||||
|
||||
try {
|
||||
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
String algo = obj.getString("algo");
|
||||
int digits = obj.getInt("digits");
|
||||
|
||||
@@ -110,7 +117,7 @@ public abstract class OtpInfo implements Serializable {
|
||||
default:
|
||||
throw new OtpInfoException("unsupported otp type: " + type);
|
||||
}
|
||||
} catch (Base32Exception | JSONException e) {
|
||||
} catch (EncodingException | JSONException e) {
|
||||
throw new OtpInfoException(e);
|
||||
}
|
||||
|
||||
@@ -127,7 +134,7 @@ public abstract class OtpInfo implements Serializable {
|
||||
}
|
||||
|
||||
OtpInfo info = (OtpInfo) o;
|
||||
return getType().equals(info.getType())
|
||||
return getTypeId().equals(info.getTypeId())
|
||||
&& Arrays.equals(getSecret(), info.getSecret())
|
||||
&& getAlgorithm(false).equals(info.getAlgorithm(false))
|
||||
&& getDigits() == info.getDigits();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user