Merge branch 'master' into mime4j

Conflicts:
	OpenKeychain/build.gradle
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
	OpenKeychain/src/main/res/values/strings.xml
This commit is contained in:
Vincent Breitmoser
2015-09-14 16:21:04 +02:00
365 changed files with 26290 additions and 6458 deletions

View File

@@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com host = https://www.transifex.com
lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw lang_map = he: iw, zh_TW: zh-rTW
[open-keychain.strings] [open-keychain.strings]
file_filter = OpenKeychain/src/main/res/values-<lang>/strings.xml file_filter = OpenKeychain/src/main/res/values-<lang>/strings.xml

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M11,10V12H10V14H8V12H5.83C5.42,13.17 4.31,14 3,14A3,3 0 0,1 0,11A3,3 0 0,1 3,8C4.31,8 5.42,8.83 5.83,10H11M3,10A1,1 0 0,0 2,11A1,1 0 0,0 3,12A1,1 0 0,0 4,11A1,1 0 0,0 3,10M16,14C18.67,14 24,15.34 24,18V20H8V18C8,15.34 13.33,14 16,14M16,12A4,4 0 0,1 12,8A4,4 0 0,1 16,4A4,4 0 0,1 20,8A4,4 0 0,1 16,12Z" /></svg>

After

Width:  |  Height:  |  Size: 595 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M16,6H13V7.9H16C18.26,7.9 20.1,9.73 20.1,12A4.1,4.1 0 0,1 16,16.1H13V18H16A6,6 0 0,0 22,12C22,8.68 19.31,6 16,6M3.9,12C3.9,9.73 5.74,7.9 8,7.9H11V6H8A6,6 0 0,0 2,12A6,6 0 0,0 8,18H11V16.1H8C5.74,16.1 3.9,14.26 3.9,12M8,13H16V11H8V13Z" /></svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M7,9A2,2 0 0,1 5,7A2,2 0 0,1 7,5A2,2 0 0,1 9,7A2,2 0 0,1 7,9M20,3H4A1,1 0 0,0 3,4V10A1,1 0 0,0 4,11H20A1,1 0 0,0 21,10V4A1,1 0 0,0 20,3M7,19A2,2 0 0,1 5,17A2,2 0 0,1 7,15A2,2 0 0,1 9,17A2,2 0 0,1 7,19M20,13H4A1,1 0 0,0 3,14V20A1,1 0 0,0 4,21H20A1,1 0 0,0 21,20V14A1,1 0 0,0 20,13Z" />
</svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M22.46,6C21.69,6.35 20.86,6.58 20,6.69C20.88,6.16 21.56,5.32 21.88,4.31C21.05,4.81 20.13,5.16 19.16,5.36C18.37,4.5 17.26,4 16,4C13.65,4 11.73,5.92 11.73,8.29C11.73,8.63 11.77,8.96 11.84,9.27C8.28,9.09 5.11,7.38 3,4.79C2.63,5.42 2.42,6.16 2.42,6.94C2.42,8.43 3.17,9.75 4.33,10.5C3.62,10.5 2.96,10.3 2.38,10C2.38,10 2.38,10 2.38,10.03C2.38,12.11 3.86,13.85 5.82,14.24C5.46,14.34 5.08,14.39 4.69,14.39C4.42,14.39 4.15,14.36 3.89,14.31C4.43,16 6,17.26 7.89,17.29C6.43,18.45 4.58,19.13 2.56,19.13C2.22,19.13 1.88,19.11 1.54,19.07C3.44,20.29 5.7,21 8.12,21C16,21 20.33,14.46 20.33,8.79C20.33,8.6 20.33,8.42 20.32,8.23C21.16,7.63 21.88,6.87 22.46,6Z" />
</svg>

After

Width:  |  Height:  |  Size: 763 B

View File

@@ -0,0 +1,3 @@
<svg height="1024" width="1024" xmlns="http://www.w3.org/2000/svg">
<path d="M256 576h64v64h-64c-96 0-192-108-192-224s99-224 192-224h256c93 0 192 108 192 224 0 90-58 174-128 208v-74c37-29 64-81 64-134 0-82-65-160-128-160H256c-63 0-128 78-128 160s64 160 128 160z m576-192h-64v64h64c64 0 128 78 128 160s-65 160-128 160H576c-63 0-128-78-128-160 0-53 27-105 64-134v-74c-70 34-128 118-128 208 0 116 99 224 192 224h256c93 0 192-108 192-224s-96-224-192-224z" />
</svg>

After

Width:  |  Height:  |  Size: 464 B

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="100px"
height="100px"
viewBox="0 0 100 100"
version="1.1"
id="svg2"
inkscape:version="0.48.5 r10040"
sodipodi:docname="status_signature_verified_cutout.svg">
<metadata
id="metadata16">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>signature-verified-cutout</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1280"
inkscape:window-height="784"
id="namedview14"
showgrid="false"
inkscape:zoom="2.36"
inkscape:cx="50"
inkscape:cy="50"
inkscape:window-x="0"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:current-layer="signature-verified-cutout" />
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title
id="title4">signature-verified-cutout</title>
<desc
id="desc6">Created with Sketch.</desc>
<defs
id="defs8" />
<g
id="Page-1"
sketch:type="MSPage"
stroke-width="1"
stroke="none"
fill-rule="evenodd"
fill="none">
<g
id="signature-verified-cutout"
sketch:type="MSArtboardGroup"
transform="translate(0.110156, 0.000000)"
fill="#000000">
<path
d="M 46.273291,77.5085 20,57.830916 27.91844,47.63497 43.309686,59.515226 70.31112,23 80.867825,30.778219 z"
sketch:type="MSShapeGroup"
id="path12"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -9,6 +9,7 @@ python copy OpenKeychain navigation black expand_more 24
python copy OpenKeychain navigation white refresh 24 python copy OpenKeychain navigation white refresh 24
python copy OpenKeychain av white repeat 24 python copy OpenKeychain av white repeat 24
python copy OpenKeychain av grey repeat 24 python copy OpenKeychain av grey repeat 24
python copy OpenKeychain av black repeat 24
python copy OpenKeychain editor white mode_edit 24 python copy OpenKeychain editor white mode_edit 24
python copy OpenKeychain content white save 24 python copy OpenKeychain content white save 24
python copy OpenKeychain navigation black close 24 python copy OpenKeychain navigation black close 24
@@ -63,3 +64,10 @@ python copy OpenKeychain action white delete 24
# yubikey dialog # yubikey dialog
python copy OpenKeychain action black check_circle 48 python copy OpenKeychain action black check_circle 48
# preference header
python copy OpenKeychain file black cloud 24 # cloud
python copy OpenKeychain action black lock 24 # password settings
python copy OpenKeychain action black settings_ethernet 24 # proxy
python copy OpenKeychain notification black sync 24 # sync
python copy OpenKeychain action black extension 24 # experimental

View File

@@ -22,7 +22,7 @@ SRC_DIR=./drawables/
#inkscape -w 512 -h 512 -e "$PLAY_DIR/$NAME.png" $NAME.svg #inkscape -w 512 -h 512 -e "$PLAY_DIR/$NAME.png" $NAME.svg
for NAME in "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "yubi_icon" "ic_stat_notify" for NAME in "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "yubi_icon" "ic_stat_notify" "status_signature_verified_inner" "link" "octo_link"
do do
echo $NAME echo $NAME
inkscape -w 24 -h 24 -e "$MDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg" inkscape -w 24 -h 24 -e "$MDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
@@ -32,7 +32,7 @@ inkscape -w 72 -h 72 -e "$XXDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
inkscape -w 96 -h 96 -e "$XXXDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg" inkscape -w 96 -h 96 -e "$XXXDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"
done done
for NAME in "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" for NAME in "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "status_signature_verified_inner"
do do
echo $NAME echo $NAME
inkscape -w 96 -h 96 -e "$MDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg" inkscape -w 96 -h 96 -e "$MDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg"
@@ -41,7 +41,7 @@ inkscape -w 192 -h 192 -e "$XDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg"
inkscape -w 256 -h 256 -e "$XXDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg" inkscape -w 256 -h 256 -e "$XXDPI_DIR/${NAME}_96dp.png" "$SRC_DIR/$NAME.svg"
done done
for NAME in "create_key_robot" for NAME in "create_key_robot" "linked_dns" "linked_https" "linked_github" "linked_twitter" "account_key"
do do
echo $NAME echo $NAME
inkscape -w 48 -h 48 -e "$MDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg" inkscape -w 48 -h 48 -e "$MDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"

View File

@@ -46,7 +46,8 @@ dependencies {
compile 'com.jpardogo.materialtabstrip:library:1.0.9' compile 'com.jpardogo.materialtabstrip:library:1.0.9'
compile 'com.getbase:floatingactionbutton:1.9.0' compile 'com.getbase:floatingactionbutton:1.9.0'
compile 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.0' compile 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.0'
compile 'com.splitwise:tokenautocomplete:1.3.3@aar' compile 'org.ocpsoft.prettytime:prettytime:3.2.7.Final'
compile "com.splitwise:tokenautocomplete:1.3.3@aar"
compile 'se.emilsjolander:stickylistheaders:2.6.0' compile 'se.emilsjolander:stickylistheaders:2.6.0'
compile 'org.sufficientlysecure:html-textview:1.2' compile 'org.sufficientlysecure:html-textview:1.2'
compile 'com.mikepenz:materialdrawer:3.0.9@aar' compile 'com.mikepenz:materialdrawer:3.0.9@aar'
@@ -58,6 +59,7 @@ dependencies {
compile 'com.squareup.okhttp:okhttp:2.4.0' compile 'com.squareup.okhttp:okhttp:2.4.0'
compile 'org.apache.james:apache-mime4j-core:0.7.2' compile 'org.apache.james:apache-mime4j-core:0.7.2'
compile 'org.apache.james:apache-mime4j-dom:0.7.2' compile 'org.apache.james:apache-mime4j-dom:0.7.2'
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
// libs as submodules // libs as submodules
compile project(':extern:openpgp-api-lib:openpgp-api') compile project(':extern:openpgp-api-lib:openpgp-api')
@@ -119,8 +121,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 22 targetSdkVersion 22
versionCode 34100 versionCode 35100
versionName "3.4.1" versionName "3.5.1"
applicationId "org.sufficientlysecure.keychain" applicationId "org.sufficientlysecure.keychain"
// the androidjunitrunner is broken regarding coverage, see here: // the androidjunitrunner is broken regarding coverage, see here:
// https://code.google.com/p/android/issues/detail?id=170607 // https://code.google.com/p/android/issues/detail?id=170607
@@ -143,9 +145,11 @@ android {
// Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE. // Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE.
buildConfigField "String", "ACCOUNT_TYPE", "\"org.sufficientlysecure.keychain.account\"" buildConfigField "String", "ACCOUNT_TYPE", "\"org.sufficientlysecure.keychain.account\""
buildConfigField "String", "PROVIDER_CONTENT_AUTHORITY", "\"org.sufficientlysecure.keychain.provider\""
// Reference them in .xml files. // Reference them in .xml files.
resValue "string", "account_type", "org.sufficientlysecure.keychain.account" resValue "string", "account_type", "org.sufficientlysecure.keychain.account"
resValue "string", "provider_content_authority", "org.sufficientlysecure.keychain.provider"
} }
debug { debug {
@@ -153,9 +157,15 @@ android {
// Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE. // Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE.
buildConfigField "String", "ACCOUNT_TYPE", "\"org.sufficientlysecure.keychain.debug.account\"" buildConfigField "String", "ACCOUNT_TYPE", "\"org.sufficientlysecure.keychain.debug.account\""
buildConfigField "String", "PROVIDER_CONTENT_AUTHORITY", "\"org.sufficientlysecure.keychain.debug.provider\""
// Github api for debug build only
buildConfigField "String", "GITHUB_CLIENT_ID", "\"7a011b66275f244d3f21\""
buildConfigField "String", "GITHUB_CLIENT_SECRET", "\"eaced8a6655719d8c6848396de97b3f5d7a89fec\""
// Reference them in .xml files. // Reference them in .xml files.
resValue "string", "account_type", "org.sufficientlysecure.keychain.debug.account" resValue "string", "account_type", "org.sufficientlysecure.keychain.debug.account"
resValue "string", "provider_content_authority", "org.sufficientlysecure.keychain.debug.provider"
// Enable code coverage (Jacoco) // Enable code coverage (Jacoco)
testCoverageEnabled true testCoverageEnabled true
@@ -202,11 +212,11 @@ android {
} }
dexOptions { dexOptions {
incremental = true
// Disable preDexing, causes com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000) on some systems // Disable preDexing, causes com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000) on some systems
preDexLibraries = false preDexLibraries = false
// faster with incremental? jumboMode = true
// incremental true javaMaxHeapSize "2g"
javaMaxHeapSize "4g"
} }
packagingOptions { packagingOptions {

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -71,7 +71,7 @@
android:name=".KeychainApplication" android:name=".KeychainApplication"
android:allowBackup="false" android:allowBackup="false"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@drawable/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Keychain.Light"> android:theme="@style/Theme.Keychain.Light">
<activity <activity
@@ -110,6 +110,13 @@
android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_edit_key" /> android:label="@string/title_edit_key" />
<!-- NOTE: Dont use configChanges for QR Code view! We use a different layout for landscape --> <!-- NOTE: Dont use configChanges for QR Code view! We use a different layout for landscape -->
<activity
android:name=".ui.linked.LinkedIdWizard"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_linked_create"
android:parentActivityName=".ui.ViewKeyActivity"
>
</activity>
<activity <activity
android:name=".ui.QrCodeViewActivity" android:name=".ui.QrCodeViewActivity"
android:label="@string/share_qr_code_dialog_title" /> android:label="@string/share_qr_code_dialog_title" />
@@ -691,7 +698,6 @@
<activity <activity
android:name=".ui.OrbotRequiredDialogActivity" android:name=".ui.OrbotRequiredDialogActivity"
android:theme="@android:style/Theme.NoDisplay" /> android:theme="@android:style/Theme.NoDisplay" />
<activity android:name=".ui.PassphraseWizardActivity" />
<!-- <!--
NOTE: singleTop is set to get NFC foreground dispatch to work. NOTE: singleTop is set to get NFC foreground dispatch to work.
Then, all NFC intents will be broadcasted to onNewIntent() of this activity! Then, all NFC intents will be broadcasted to onNewIntent() of this activity!
@@ -701,7 +707,7 @@
--> -->
<activity <activity
android:name=".ui.NfcOperationActivity" android:name=".ui.NfcOperationActivity"
android:theme="@style/Theme.Keychain.Light.Dialog.SecurityToken" android:theme="@style/Theme.Keychain.Light.Dialog"
android:allowTaskReparenting="true" android:allowTaskReparenting="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity=":Nfc" /> android:taskAffinity=":Nfc" />
@@ -723,10 +729,13 @@
android:name=".service.KeychainService" android:name=".service.KeychainService"
android:exported="false" /> android:exported="false" />
<!-- label is made to be "Keyserver Sync" since that is the only context in which
the user will see it-->
<provider <provider
android:name=".provider.KeychainProvider" android:name=".provider.KeychainProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
android:exported="false" /> android:exported="false"
android:label="@string/keyserver_sync_settings_title"/>
<!-- Internal classes of the remote APIs (not exported) --> <!-- Internal classes of the remote APIs (not exported) -->
<activity <activity
@@ -758,8 +767,9 @@
android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:exported="false" /> android:exported="false" />
<!-- OpenPGP Remote API, this service has explicitly no permission requirements <!-- DEPRECATED service,
because we are using our own package based allow/disallow system --> using this service may lead to truncated data being returned to the caller
-->
<service <service
android:name=".remote.OpenPgpService" android:name=".remote.OpenPgpService"
android:enabled="true" android:enabled="true"
@@ -771,6 +781,19 @@
</intent-filter> </intent-filter>
</service> </service>
<!-- OpenPGP Remote API, this service has explicitly no permission requirements
because we are using our own package based allow/disallow system -->
<service
android:name=".remote.OpenPgpService2"
android:enabled="true"
android:exported="true"
android:process=":remote_api_2"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="org.openintents.openpgp.IOpenPgpService2" />
</intent-filter>
</service>
<!-- Contact Sync services --> <!-- Contact Sync services -->
<service <service
android:name=".service.DummyAccountService" android:name=".service.DummyAccountService"
@@ -800,6 +823,20 @@
android:resource="@xml/custom_pgp_contacts_structure" /> android:resource="@xml/custom_pgp_contacts_structure" />
</service> </service>
<service
android:name=".service.KeyserverSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/keyserver_sync_adapter_desc" />
</service>
<!-- Storage Provider for temporary decrypted files --> <!-- Storage Provider for temporary decrypted files -->
<provider <provider
android:name=".provider.TemporaryStorageProvider" android:name=".provider.TemporaryStorageProvider"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -19,14 +19,9 @@ package org.sufficientlysecure.keychain;
import android.os.Environment; import android.os.Environment;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.spongycastle.jce.provider.BouncyCastleProvider; import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.sufficientlysecure.keychain.BuildConfig;
import java.io.File; import java.io.File;
import java.net.InetSocketAddress;
import java.net.Proxy; import java.net.Proxy;
public final class Constants { public final class Constants {
@@ -34,6 +29,7 @@ public final class Constants {
public static final boolean DEBUG = BuildConfig.DEBUG; public static final boolean DEBUG = BuildConfig.DEBUG;
public static final boolean DEBUG_LOG_DB_QUERIES = false; public static final boolean DEBUG_LOG_DB_QUERIES = false;
public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false; public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false;
public static final boolean DEBUG_KEYSERVER_SYNC = false;
public static final String TAG = DEBUG ? "Keychain D" : "Keychain"; public static final String TAG = DEBUG ? "Keychain D" : "Keychain";
@@ -43,7 +39,7 @@ public final class Constants {
public static final String ACCOUNT_TYPE = BuildConfig.ACCOUNT_TYPE; public static final String ACCOUNT_TYPE = BuildConfig.ACCOUNT_TYPE;
public static final String CUSTOM_CONTACT_DATA_MIME_TYPE = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.key"; public static final String CUSTOM_CONTACT_DATA_MIME_TYPE = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.key";
public static final String PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider"; public static final String PROVIDER_AUTHORITY = BuildConfig.PROVIDER_CONTENT_AUTHORITY;
public static final String TEMPSTORAGE_AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage"; public static final String TEMPSTORAGE_AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage";
public static final String CLIPBOARD_LABEL = "Keychain"; public static final String CLIPBOARD_LABEL = "Keychain";
@@ -81,6 +77,11 @@ public final class Constants {
public static final File APP_DIR_FILE = new File(APP_DIR, "export.asc"); public static final File APP_DIR_FILE = new File(APP_DIR, "export.asc");
} }
public static final class Notification {
public static final int PASSPHRASE_CACHE = 1;
public static final int KEYSERVER_SYNC_FAIL_ORBOT = 2;
}
public static final class Pref { public static final class Pref {
public static final String PASSPHRASE_CACHE_TTL = "passphraseCacheTtl"; public static final String PASSPHRASE_CACHE_TTL = "passphraseCacheTtl";
public static final String PASSPHRASE_CACHE_SUBS = "passphraseCacheSubs"; public static final String PASSPHRASE_CACHE_SUBS = "passphraseCacheSubs";
@@ -104,12 +105,24 @@ public final class Constants {
public static final String PROXY_PORT = "proxyPort"; public static final String PROXY_PORT = "proxyPort";
public static final String PROXY_TYPE = "proxyType"; public static final String PROXY_TYPE = "proxyType";
public static final String THEME = "theme"; public static final String THEME = "theme";
// keyserver sync settings
public static final String SYNC_CONTACTS = "syncContacts";
public static final String SYNC_KEYSERVER = "syncKeyserver";
// other settings
public static final String EXPERIMENTAL_ENABLE_WORD_CONFIRM = "experimentalEnableWordConfirm";
public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities";
public static final String EXPERIMENTAL_ENABLE_KEYBASE = "experimentalEnableKeybase";
public static final class Theme { public static final class Theme {
public static final String LIGHT = "light"; public static final String LIGHT = "light";
public static final String DARK = "dark"; public static final String DARK = "dark";
public static final String DEFAULT = Constants.Pref.Theme.LIGHT; public static final String DEFAULT = Constants.Pref.Theme.LIGHT;
} }
public static final class ProxyType {
public static final String TYPE_HTTP = "proxyHttp";
public static final String TYPE_SOCKS = "proxySocks";
}
} }
/** /**
@@ -123,7 +136,7 @@ public final class Constants {
public static final class Defaults { public static final class Defaults {
public static final String KEY_SERVERS = "hkps://hkps.pool.sks-keyservers.net, hkps://pgp.mit.edu"; public static final String KEY_SERVERS = "hkps://hkps.pool.sks-keyservers.net, hkps://pgp.mit.edu";
public static final int PREF_VERSION = 5; public static final int PREF_VERSION = 6;
} }
public static final class key { public static final class key {

View File

@@ -23,7 +23,6 @@ import android.app.Application;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@@ -35,7 +34,7 @@ import android.widget.Toast;
import org.spongycastle.jce.provider.BouncyCastleProvider; import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity; import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
@@ -97,7 +96,7 @@ public class KeychainApplication extends Application {
setupAccountAsNeeded(this); setupAccountAsNeeded(this);
// Update keyserver list as needed // Update keyserver list as needed
Preferences.getPreferences(this).upgradePreferences(); Preferences.getPreferences(this).upgradePreferences(this);
TlsHelper.addStaticCA("pool.sks-keyservers.net", getAssets(), "sks-keyservers.netCA.cer"); TlsHelper.addStaticCA("pool.sks-keyservers.net", getAssets(), "sks-keyservers.netCA.cer");
@@ -136,17 +135,20 @@ public class KeychainApplication extends Application {
} }
/** /**
* Add OpenKeychain account to Android to link contacts with keys * Add OpenKeychain account to Android to link contacts with keys and keyserver sync
*/ */
public static void setupAccountAsNeeded(Context context) { public static void setupAccountAsNeeded(Context context) {
try { try {
AccountManager manager = AccountManager.get(context); AccountManager manager = AccountManager.get(context);
Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE); Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE);
if (accounts == null || accounts.length == 0) {
if (accounts.length == 0) {
Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE); Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE);
if (manager.addAccountExplicitly(account, null, null)) { if (manager.addAccountExplicitly(account, null, null)) {
// for contact sync
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
KeyserverSyncAdapterService.enableKeyserverSync(context);
} else { } else {
Log.e(Constants.TAG, "Adding account failed!"); Log.e(Constants.TAG, "Adding account failed!");
} }

View File

@@ -196,9 +196,9 @@ public class HkpKeyserver extends Keyserver {
/** /**
* returns a client with pinned certificate if necessary * returns a client with pinned certificate if necessary
* *
* @param url * @param url url to be queried by client
* @param proxy * @param proxy proxy to be used by client
* @return * @return client with a pinned certificate if necesary
*/ */
public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException { public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException {
OkHttpClient client = new OkHttpClient(); OkHttpClient client = new OkHttpClient();
@@ -360,7 +360,7 @@ public class HkpKeyserver extends Keyserver {
try { try {
data = query(request, proxy); data = query(request, proxy);
} catch (HttpError httpError) { } catch (HttpError httpError) {
Log.e(Constants.TAG, "Failed to get key at HkpKeyserver", httpError); Log.d(Constants.TAG, "Failed to get key at HkpKeyserver", httpError);
throw new QueryFailedException("not found"); throw new QueryFailedException("not found");
} }
Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data); Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data);

View File

@@ -0,0 +1,32 @@
package org.sufficientlysecure.keychain.linked;
import java.net.URI;
import android.content.Context;
import android.support.annotation.DrawableRes;
public class LinkedAttribute extends UriAttribute {
public final LinkedResource mResource;
protected LinkedAttribute(URI uri, LinkedResource resource) {
super(uri);
if (resource == null) {
throw new AssertionError("resource must not be null in a LinkedIdentity!");
}
mResource = resource;
}
public @DrawableRes int getDisplayIcon() {
return mResource.getDisplayIcon();
}
public String getDisplayTitle(Context context) {
return mResource.getDisplayTitle(context);
}
public String getDisplayComment(Context context) {
return mResource.getDisplayComment(context);
}
}

View File

@@ -0,0 +1,25 @@
package org.sufficientlysecure.keychain.linked;
import java.net.URI;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
public abstract class LinkedResource {
public abstract URI toUri();
public abstract @DrawableRes int getDisplayIcon();
public abstract @StringRes int getVerifiedText(boolean isSecret);
public abstract String getDisplayTitle(Context context);
public abstract String getDisplayComment(Context context);
public boolean isViewable() {
return false;
}
public Intent getViewIntent() {
return null;
}
}

View File

@@ -0,0 +1,300 @@
package org.sufficientlysecure.keychain.linked;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.json.JSONException;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.linked.resources.DnsResource;
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
import org.sufficientlysecure.keychain.linked.resources.GithubResource;
import org.sufficientlysecure.keychain.linked.resources.TwitterResource;
import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
import org.thoughtcrime.ssl.pinning.util.PinningHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.Context;
public abstract class LinkedTokenResource extends LinkedResource {
protected final URI mSubUri;
protected final Set<String> mFlags;
protected final HashMap<String,String> mParams;
public static Pattern magicPattern =
Pattern.compile("\\[Verifying my (?:Open|)?PGP key(?::|) openpgp4fpr:([a-zA-Z0-9]+)]");
protected LinkedTokenResource(Set<String> flags, HashMap<String, String> params, URI uri) {
mFlags = flags;
mParams = params;
mSubUri = uri;
}
@SuppressWarnings("unused")
public URI getSubUri () {
return mSubUri;
}
public Set<String> getFlags () {
return new HashSet<>(mFlags);
}
public HashMap<String,String> getParams () {
return new HashMap<>(mParams);
}
public static String generate (byte[] fingerprint) {
return String.format("[Verifying my OpenPGP key: openpgp4fpr:%s]",
KeyFormattingUtils.convertFingerprintToHex(fingerprint));
}
protected static LinkedTokenResource fromUri (URI uri) {
if (!"openpgpid+token".equals(uri.getScheme())
&& !"openpgpid+cookie".equals(uri.getScheme())) {
Log.e(Constants.TAG, "unknown uri scheme in (suspected) linked id packet");
return null;
}
if (!uri.isOpaque()) {
Log.e(Constants.TAG, "non-opaque uri in (suspected) linked id packet");
return null;
}
String specific = uri.getSchemeSpecificPart();
if (!specific.contains("@")) {
Log.e(Constants.TAG, "unknown uri scheme in linked id packet");
return null;
}
String[] pieces = specific.split("@", 2);
URI subUri = URI.create(pieces[1]);
Set<String> flags = new HashSet<>();
HashMap<String,String> params = new HashMap<>();
if (!pieces[0].isEmpty()) {
String[] rawParams = pieces[0].split(";");
for (String param : rawParams) {
String[] p = param.split("=", 2);
if (p.length == 1) {
flags.add(param);
} else {
params.put(p[0], p[1]);
}
}
}
return findResourceType(flags, params, subUri);
}
protected static LinkedTokenResource findResourceType (Set<String> flags,
HashMap<String,String> params, URI subUri) {
LinkedTokenResource res;
res = GenericHttpsResource.create(flags, params, subUri);
if (res != null) {
return res;
}
// res = DnsResource.create(flags, params, subUri);
// if (res != null) {
// return res;
// }
res = TwitterResource.create(flags, params, subUri);
if (res != null) {
return res;
}
res = GithubResource.create(flags, params, subUri);
if (res != null) {
return res;
}
return null;
}
public URI toUri () {
StringBuilder b = new StringBuilder();
b.append("openpgpid+token:");
// add flags
if (mFlags != null) {
boolean first = true;
for (String flag : mFlags) {
if (!first) {
b.append(";");
}
first = false;
b.append(flag);
}
}
// add parameters
if (mParams != null) {
boolean first = true;
for (Entry<String, String> stringStringEntry : mParams.entrySet()) {
if (!first) {
b.append(";");
}
first = false;
b.append(stringStringEntry.getKey()).append("=").append(stringStringEntry.getValue());
}
}
b.append("@");
b.append(mSubUri);
return URI.create(b.toString());
}
public LinkedVerifyResult verify(Context context, byte[] fingerprint) {
OperationLog log = new OperationLog();
log.add(LogType.MSG_LV, 0);
// Try to fetch resource. Logs for itself
String res = null;
try {
res = fetchResource(context, log, 1);
} catch (HttpStatusException e) {
// log verbose output to logcat
Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason());
log.add(LogType.MSG_LV_FETCH_ERROR, 2, Integer.toString(e.getStatus()));
} catch (MalformedURLException e) {
log.add(LogType.MSG_LV_FETCH_ERROR_URL, 2);
} catch (IOException e) {
Log.e(Constants.TAG, "io error", e);
log.add(LogType.MSG_LV_FETCH_ERROR_IO, 2);
} catch (JSONException e) {
Log.e(Constants.TAG, "json error", e);
log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 2);
}
if (res == null) {
// if this is null, an error was recorded in fetchResource above
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
}
Log.d(Constants.TAG, "Resource data: '" + res + "'");
return verifyString(log, 1, res, fingerprint);
}
protected abstract String fetchResource (Context context, OperationLog log, int indent)
throws HttpStatusException, IOException, JSONException;
protected Matcher matchResource (OperationLog log, int indent, String res) {
return magicPattern.matcher(res);
}
protected LinkedVerifyResult verifyString (OperationLog log, int indent,
String res,
byte[] fingerprint) {
log.add(LogType.MSG_LV_MATCH, indent);
Matcher match = matchResource(log, indent+1, res);
if (!match.find()) {
log.add(LogType.MSG_LV_MATCH_ERROR, 2);
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
}
String candidateFp = match.group(1).toLowerCase();
String fp = KeyFormattingUtils.convertFingerprintToHex(fingerprint);
if (!fp.equals(candidateFp)) {
log.add(LogType.MSG_LV_FP_ERROR, indent);
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
}
log.add(LogType.MSG_LV_FP_OK, indent);
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_OK, log);
}
@SuppressWarnings("deprecation") // HttpRequestBase is deprecated
public static String getResponseBody(Context context, HttpRequestBase request)
throws IOException, HttpStatusException {
return getResponseBody(context, request, null);
}
@SuppressWarnings("deprecation") // HttpRequestBase is deprecated
public static String getResponseBody(Context context, HttpRequestBase request, String[] pins)
throws IOException, HttpStatusException {
StringBuilder sb = new StringBuilder();
request.setHeader("User-Agent", "Open Keychain");
HttpClient httpClient;
if (pins == null) {
httpClient = new DefaultHttpClient(new BasicHttpParams());
} else {
httpClient = PinningHelper.getPinnedHttpClient(context, pins);
}
HttpResponse response = httpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
String reason = response.getStatusLine().getReasonPhrase();
if (statusCode != 200) {
throw new HttpStatusException(statusCode, reason);
}
HttpEntity entity = response.getEntity();
InputStream inputStream = entity.getContent();
BufferedReader bReader = new BufferedReader(
new InputStreamReader(inputStream, "UTF-8"), 8);
String line;
while ((line = bReader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
public static class HttpStatusException extends Throwable {
private final int mStatusCode;
private final String mReason;
HttpStatusException(int statusCode, String reason) {
super("http status " + statusCode + ": " + reason);
mStatusCode = statusCode;
mReason = reason;
}
public int getStatus() {
return mStatusCode;
}
public String getReason() {
return mReason;
}
}
}

View File

@@ -0,0 +1,79 @@
package org.sufficientlysecure.keychain.linked;
import org.spongycastle.util.Strings;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.util.Log;
import java.io.IOException;
import java.net.URI;
import android.content.Context;
import android.support.annotation.DrawableRes;
/** The RawLinkedIdentity contains raw parsed data from a Linked Identity subpacket. */
public class UriAttribute {
public final URI mUri;
protected UriAttribute(URI uri) {
mUri = uri;
}
public byte[] getEncoded() {
return Strings.toUTF8ByteArray(mUri.toASCIIString());
}
public static UriAttribute fromAttributeData(byte[] data) throws IOException {
WrappedUserAttribute att = WrappedUserAttribute.fromData(data);
byte[][] subpackets = att.getSubpackets();
if (subpackets.length >= 1) {
return fromSubpacketData(subpackets[0]);
}
throw new IOException("no subpacket data");
}
static UriAttribute fromSubpacketData(byte[] data) {
try {
String uriStr = Strings.fromUTF8ByteArray(data);
URI uri = URI.create(uriStr);
LinkedResource res = LinkedTokenResource.fromUri(uri);
if (res == null) {
return new UriAttribute(uri);
}
return new LinkedAttribute(uri, res);
} catch (IllegalArgumentException e) {
Log.e(Constants.TAG, "error parsing uri in (suspected) linked id packet");
return null;
}
}
public static UriAttribute fromResource (LinkedTokenResource res) {
return new UriAttribute(res.toUri());
}
public WrappedUserAttribute toUserAttribute () {
return WrappedUserAttribute.fromSubpacket(WrappedUserAttribute.UAT_URI_ATTRIBUTE, getEncoded());
}
public @DrawableRes int getDisplayIcon() {
return R.drawable.ic_warning_grey_24dp;
}
public String getDisplayTitle(Context context) {
return "Unknown Identity";
}
public String getDisplayComment(Context context) {
return null;
}
}

View File

@@ -0,0 +1,130 @@
package org.sufficientlysecure.keychain.linked.resources;
import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.measite.minidns.Client;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.Question;
import de.measite.minidns.Record;
import de.measite.minidns.Record.CLASS;
import de.measite.minidns.Record.TYPE;
import de.measite.minidns.record.TXT;
public class DnsResource extends LinkedTokenResource {
final static Pattern magicPattern =
Pattern.compile("openpgpid\\+token=([a-zA-Z0-9]+)(?:#|;)([a-zA-Z0-9]+)");
String mFqdn;
CLASS mClass;
TYPE mType;
DnsResource(Set<String> flags, HashMap<String, String> params, URI uri,
String fqdn, CLASS clazz, TYPE type) {
super(flags, params, uri);
mFqdn = fqdn;
mClass = clazz;
mType = type;
}
public static String generateText(byte[] fingerprint) {
return String.format("openpgp4fpr=%s",
KeyFormattingUtils.convertFingerprintToHex(fingerprint));
}
public static DnsResource createNew (String domain) {
HashSet<String> flags = new HashSet<>();
HashMap<String,String> params = new HashMap<>();
URI uri = URI.create("dns:" + domain + "?TYPE=TXT");
return create(flags, params, uri);
}
public static DnsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
if ( ! ("dns".equals(uri.getScheme())
&& (flags == null || flags.isEmpty())
&& (params == null || params.isEmpty()))) {
return null;
}
//
String spec = uri.getSchemeSpecificPart();
// If there are // at the beginning, this includes an authority - we don't support those!
if (spec.startsWith("//")) {
return null;
}
String[] pieces = spec.split("\\?", 2);
// In either case, part before a ? is the fqdn
String fqdn = pieces[0];
// There may be a query part
/*
if (pieces.length > 1) {
// parse CLASS and TYPE query paramters
}
*/
CLASS clazz = CLASS.IN;
TYPE type = TYPE.TXT;
return new DnsResource(flags, params, uri, fqdn, clazz, type);
}
@SuppressWarnings("unused")
public String getFqdn() {
return mFqdn;
}
@Override
protected String fetchResource (Context context, OperationLog log, int indent) {
Client c = new Client();
DNSMessage msg = c.query(new Question(mFqdn, mType, mClass));
Record aw = msg.getAnswers()[0];
TXT txt = (TXT) aw.getPayload();
return txt.getText().toLowerCase();
}
@Override
protected Matcher matchResource(OperationLog log, int indent, String res) {
return magicPattern.matcher(res);
}
@Override
public @StringRes
int getVerifiedText(boolean isSecret) {
return isSecret ? R.string.linked_verified_secret_dns : R.string.linked_verified_dns;
}
@Override
public @DrawableRes int getDisplayIcon() {
return R.drawable.linked_dns;
}
@Override
public String getDisplayTitle(Context context) {
return context.getString(R.string.linked_title_dns);
}
@Override
public String getDisplayComment(Context context) {
return mFqdn;
}
}

View File

@@ -0,0 +1,95 @@
package org.sufficientlysecure.keychain.linked.resources;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import org.apache.http.client.methods.HttpGet;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
public class GenericHttpsResource extends LinkedTokenResource {
GenericHttpsResource(Set<String> flags, HashMap<String,String> params, URI uri) {
super(flags, params, uri);
}
public static String generateText (Context context, byte[] fingerprint) {
String token = LinkedTokenResource.generate(fingerprint);
return String.format(context.getResources().getString(R.string.linked_id_generic_text),
token, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
}
@SuppressWarnings("deprecation") // HttpGet is deprecated
@Override
protected String fetchResource (Context context, OperationLog log, int indent)
throws HttpStatusException, IOException {
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
HttpGet httpGet = new HttpGet(mSubUri);
return getResponseBody(context, httpGet);
}
public static GenericHttpsResource createNew (URI uri) {
HashSet<String> flags = new HashSet<>();
flags.add("generic");
HashMap<String,String> params = new HashMap<>();
return create(flags, params, uri);
}
public static GenericHttpsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
if ( ! ("https".equals(uri.getScheme())
&& flags != null && flags.size() == 1 && flags.contains("generic")
&& (params == null || params.isEmpty()))) {
return null;
}
return new GenericHttpsResource(flags, params, uri);
}
@Override
public @DrawableRes
int getDisplayIcon() {
return R.drawable.linked_https;
}
@Override
public @StringRes
int getVerifiedText(boolean isSecret) {
return isSecret ? R.string.linked_verified_secret_https : R.string.linked_verified_https;
}
@Override
public String getDisplayTitle(Context context) {
return context.getString(R.string.linked_title_https);
}
@Override
public String getDisplayComment(Context context) {
return mSubUri.toString();
}
@Override
public boolean isViewable() {
return true;
}
@Override
public Intent getViewIntent() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(mSubUri.toString()));
return intent;
}
}

View File

@@ -0,0 +1,218 @@
package org.sufficientlysecure.keychain.linked.resources;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import org.apache.http.client.methods.HttpGet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.util.Log;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GithubResource extends LinkedTokenResource {
final String mHandle;
final String mGistId;
GithubResource(Set<String> flags, HashMap<String,String> params, URI uri,
String handle, String gistId) {
super(flags, params, uri);
mHandle = handle;
mGistId = gistId;
}
public static String generate(Context context, byte[] fingerprint) {
String token = LinkedTokenResource.generate(fingerprint);
return String.format(context.getResources().getString(R.string.linked_id_github_text), token);
}
@SuppressWarnings("deprecation") // HttpGet is deprecated
@Override
protected String fetchResource (Context context, OperationLog log, int indent)
throws HttpStatusException, IOException, JSONException {
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
indent += 1;
HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + mGistId);
String response = getResponseBody(context, httpGet);
JSONObject obj = new JSONObject(response);
JSONObject owner = obj.getJSONObject("owner");
if (!mHandle.equals(owner.getString("login"))) {
log.add(LogType.MSG_LV_ERROR_GITHUB_HANDLE, indent);
return null;
}
JSONObject files = obj.getJSONObject("files");
Iterator<String> it = files.keys();
if (it.hasNext()) {
// TODO can there be multiple candidates?
JSONObject file = files.getJSONObject(it.next());
return file.getString("content");
}
log.add(LogType.MSG_LV_ERROR_GITHUB_NOT_FOUND, indent);
return null;
}
@Deprecated // not used for now, but could be used to pick up earlier posted gist if already present?
@SuppressWarnings({ "deprecation", "unused" })
public static GithubResource searchInGithubStream(
Context context, String screenName, String needle, OperationLog log) {
// narrow the needle down to important part
Matcher matcher = magicPattern.matcher(needle);
if (!matcher.find()) {
throw new AssertionError("Needle must contain token pattern! This is a programming error, please report.");
}
needle = matcher.group();
try {
JSONArray array; {
HttpGet httpGet =
new HttpGet("https://api.github.com/users/" + screenName + "/gists");
httpGet.setHeader("Content-Type", "application/json");
httpGet.setHeader("User-Agent", "OpenKeychain");
String response = getResponseBody(context, httpGet);
array = new JSONArray(response);
}
for (int i = 0, j = Math.min(array.length(), 5); i < j; i++) {
JSONObject obj = array.getJSONObject(i);
JSONObject files = obj.getJSONObject("files");
Iterator<String> it = files.keys();
if (it.hasNext()) {
JSONObject file = files.getJSONObject(it.next());
String type = file.getString("type");
if (!"text/plain".equals(type)) {
continue;
}
String id = obj.getString("id");
HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + id);
httpGet.setHeader("User-Agent", "OpenKeychain");
JSONObject gistObj = new JSONObject(getResponseBody(context, httpGet));
JSONObject gistFiles = gistObj.getJSONObject("files");
Iterator<String> gistIt = gistFiles.keys();
if (!gistIt.hasNext()) {
continue;
}
// TODO can there be multiple candidates?
JSONObject gistFile = gistFiles.getJSONObject(gistIt.next());
String content = gistFile.getString("content");
if (!content.contains(needle)) {
continue;
}
URI uri = URI.create("https://gist.github.com/" + screenName + "/" + id);
return create(uri);
}
}
// update the results with the body of the response
log.add(LogType.MSG_LV_FETCH_ERROR_NOTHING, 2);
return null;
} catch (HttpStatusException e) {
// log verbose output to logcat
Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason());
log.add(LogType.MSG_LV_FETCH_ERROR, 2, Integer.toString(e.getStatus()));
} catch (MalformedURLException e) {
log.add(LogType.MSG_LV_FETCH_ERROR_URL, 2);
} catch (IOException e) {
Log.e(Constants.TAG, "io error", e);
log.add(LogType.MSG_LV_FETCH_ERROR_IO, 2);
} catch (JSONException e) {
Log.e(Constants.TAG, "json error", e);
log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 2);
}
return null;
}
public static GithubResource create(URI uri) {
return create(new HashSet<String>(), new HashMap<String,String>(), uri);
}
public static GithubResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
// no params or flags
if (!flags.isEmpty() || !params.isEmpty()) {
return null;
}
Pattern p = Pattern.compile("https://gist\\.github\\.com/([a-zA-Z0-9_]+)/([0-9a-f]+)");
Matcher match = p.matcher(uri.toString());
if (!match.matches()) {
return null;
}
String handle = match.group(1);
String gistId = match.group(2);
return new GithubResource(flags, params, uri, handle, gistId);
}
@Override
public @DrawableRes
int getDisplayIcon() {
return R.drawable.linked_github;
}
@Override
public @StringRes
int getVerifiedText(boolean isSecret) {
return isSecret ? R.string.linked_verified_secret_github : R.string.linked_verified_github;
}
@Override
public String getDisplayTitle(Context context) {
return context.getString(R.string.linked_title_github);
}
@Override
public String getDisplayComment(Context context) {
return mHandle;
}
@Override
public boolean isViewable() {
return true;
}
@Override
public Intent getViewIntent() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(mSubUri.toString()));
return intent;
}
}

View File

@@ -0,0 +1,250 @@
package org.sufficientlysecure.keychain.linked.resources;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import android.util.Log;
import com.textuality.keybase.lib.JWalk;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TwitterResource extends LinkedTokenResource {
public static final String[] CERT_PINS = null; /*(new String[] {
// Symantec Class 3 Secure Server CA - G4
"513fb9743870b73440418d30930699ff"
};*/
final String mHandle;
final String mTweetId;
TwitterResource(Set<String> flags, HashMap<String,String> params,
URI uri, String handle, String tweetId) {
super(flags, params, uri);
mHandle = handle;
mTweetId = tweetId;
}
public static TwitterResource create(URI uri) {
return create(new HashSet<String>(), new HashMap<String,String>(), uri);
}
public static TwitterResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
// no params or flags
if (!flags.isEmpty() || !params.isEmpty()) {
return null;
}
Pattern p = Pattern.compile("https://twitter\\.com/([a-zA-Z0-9_]+)/status/([0-9]+)");
Matcher match = p.matcher(uri.toString());
if (!match.matches()) {
return null;
}
String handle = match.group(1);
String tweetId = match.group(2);
return new TwitterResource(flags, params, uri, handle, tweetId);
}
@SuppressWarnings("deprecation")
@Override
protected String fetchResource(Context context, OperationLog log, int indent)
throws IOException, HttpStatusException, JSONException {
String authToken;
try {
authToken = getAuthToken(context);
} catch (IOException | HttpStatusException | JSONException e) {
log.add(LogType.MSG_LV_ERROR_TWITTER_AUTH, indent);
return null;
}
HttpGet httpGet =
new HttpGet("https://api.twitter.com/1.1/statuses/show.json"
+ "?id=" + mTweetId
+ "&include_entities=false");
// construct a normal HTTPS request and include an Authorization
// header with the value of Bearer <>
httpGet.setHeader("Authorization", "Bearer " + authToken);
httpGet.setHeader("Content-Type", "application/json");
try {
String response = getResponseBody(context, httpGet, CERT_PINS);
JSONObject obj = new JSONObject(response);
JSONObject user = obj.getJSONObject("user");
if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) {
log.add(LogType.MSG_LV_ERROR_TWITTER_HANDLE, indent);
return null;
}
// update the results with the body of the response
return obj.getString("text");
} catch (JSONException e) {
log.add(LogType.MSG_LV_ERROR_TWITTER_RESPONSE, indent);
return null;
}
}
@Override
public @DrawableRes int getDisplayIcon() {
return R.drawable.linked_twitter;
}
@Override
public @StringRes
int getVerifiedText(boolean isSecret) {
return isSecret ? R.string.linked_verified_secret_twitter : R.string.linked_verified_twitter;
}
@Override
public String getDisplayTitle(Context context) {
return context.getString(R.string.linked_title_twitter);
}
@Override
public String getDisplayComment(Context context) {
return "@" + mHandle;
}
@Override
public boolean isViewable() {
return true;
}
@Override
public Intent getViewIntent() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(mSubUri.toString()));
return intent;
}
@SuppressWarnings("deprecation")
public static TwitterResource searchInTwitterStream(
Context context, String screenName, String needle, OperationLog log) {
String authToken;
try {
authToken = getAuthToken(context);
} catch (IOException | HttpStatusException | JSONException e) {
log.add(LogType.MSG_LV_ERROR_TWITTER_AUTH, 1);
return null;
}
HttpGet httpGet =
new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json"
+ "?screen_name=" + screenName
+ "&count=15"
+ "&include_rts=false"
+ "&trim_user=true"
+ "&exclude_replies=true");
// construct a normal HTTPS request and include an Authorization
// header with the value of Bearer <>
httpGet.setHeader("Authorization", "Bearer " + authToken);
httpGet.setHeader("Content-Type", "application/json");
try {
String response = getResponseBody(context, httpGet, CERT_PINS);
JSONArray array = new JSONArray(response);
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
String tweet = obj.getString("text");
if (tweet.contains(needle)) {
String id = obj.getString("id_str");
URI uri = URI.create("https://twitter.com/" + screenName + "/status/" + id);
return create(uri);
}
}
// update the results with the body of the response
log.add(LogType.MSG_LV_FETCH_ERROR_NOTHING, 1);
return null;
} catch (HttpStatusException e) {
// log verbose output to logcat
Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason());
log.add(LogType.MSG_LV_FETCH_ERROR, 1, Integer.toString(e.getStatus()));
} catch (MalformedURLException e) {
log.add(LogType.MSG_LV_FETCH_ERROR_URL, 1);
} catch (IOException e) {
Log.e(Constants.TAG, "io error", e);
log.add(LogType.MSG_LV_FETCH_ERROR_IO, 1);
} catch (JSONException e) {
Log.e(Constants.TAG, "json error", e);
log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 1);
}
return null;
}
private static String cachedAuthToken;
@SuppressWarnings("deprecation")
private static String getAuthToken(Context context)
throws IOException, HttpStatusException, JSONException {
if (cachedAuthToken != null) {
return cachedAuthToken;
}
String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c"
+ "Fq1bmqSAQAz5MI2cIHKOuo3cPoRAQI1OyqmIVFJS6LHMXq2g6MRLkIj") + "==";
// Step 2: Obtain a bearer token
HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token");
httpPost.setHeader("Authorization", "Basic " + base64Encoded);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
httpPost.setEntity(new StringEntity("grant_type=client_credentials"));
JSONObject rawAuthorization = new JSONObject(getResponseBody(context, httpPost, CERT_PINS));
// Applications should verify that the value associated with the
// token_type key of the returned object is bearer
if (!"bearer".equals(JWalk.getString(rawAuthorization, "token_type"))) {
throw new JSONException("Expected bearer token in response!");
}
cachedAuthToken = rawAuthorization.getString("access_token");
return cachedAuthToken;
}
public static String rot13(String input) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (c >= 'a' && c <= 'm') c += 13;
else if (c >= 'A' && c <= 'M') c += 13;
else if (c >= 'n' && c <= 'z') c -= 13;
else if (c >= 'N' && c <= 'Z') c -= 13;
sb.append(c);
}
return sb.toString();
}
}

View File

@@ -145,6 +145,8 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> {
return new ExportResult(ExportResult.RESULT_ERROR, log); return new ExportResult(ExportResult.RESULT_ERROR, log);
} }
log.add(LogType.MSG_EXPORT_FILE_NAME, 1, outputFile);
// check if storage is ready // check if storage is ready
if (!FileHelper.isStorageMounted(outputFile)) { if (!FileHelper.isStorageMounted(outputFile)) {
log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1);

View File

@@ -22,9 +22,8 @@ package org.sufficientlysecure.keychain.operations;
import java.io.IOException; import java.io.IOException;
import java.net.Proxy; import java.net.Proxy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.GregorianCalendar;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorCompletionService;
@@ -99,29 +98,9 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
return serialKeyRingImport(entries, num, keyServerUri, mProgressable, proxy); return serialKeyRingImport(entries, num, keyServerUri, mProgressable, proxy);
} }
public ImportKeyResult serialKeyRingImport(List<ParcelableKeyRing> entries,
String keyServerUri, Proxy proxy) {
Iterator<ParcelableKeyRing> it = entries.iterator();
int numEntries = entries.size();
return serialKeyRingImport(it, numEntries, keyServerUri, mProgressable, proxy);
}
public ImportKeyResult serialKeyRingImport(List<ParcelableKeyRing> entries, String keyServerUri,
Progressable progressable, Proxy proxy) {
Iterator<ParcelableKeyRing> it = entries.iterator();
int numEntries = entries.size();
return serialKeyRingImport(it, numEntries, keyServerUri, progressable, proxy);
}
@NonNull @NonNull
public ImportKeyResult serialKeyRingImport(ParcelableFileCache<ParcelableKeyRing> cache, private ImportKeyResult serialKeyRingImport(ParcelableFileCache<ParcelableKeyRing> cache,
String keyServerUri, Proxy proxy) { String keyServerUri, Proxy proxy) {
// get entries from cached file // get entries from cached file
try { try {
@@ -143,7 +122,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
/** /**
* Since the introduction of multithreaded import, we expect calling functions to handle the * Since the introduction of multithreaded import, we expect calling functions to handle the
* key sync i,eContactSyncAdapterService.requestSync() * contact-to-key sync i.e ContactSyncAdapterService.requestSync()
* *
* @param entries keys to import * @param entries keys to import
* @param num number of keys to import * @param num number of keys to import
@@ -152,9 +131,9 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
* progress of a single key being imported * progress of a single key being imported
*/ */
@NonNull @NonNull
public ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num, private ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num,
String keyServerUri, Progressable progressable, String keyServerUri, Progressable progressable,
Proxy proxy) { Proxy proxy) {
if (progressable != null) { if (progressable != null) {
progressable.setProgress(R.string.progress_importing, 0, 100); progressable.setProgress(R.string.progress_importing, 0, 100);
} }
@@ -231,8 +210,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3); log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3);
} }
} catch (Keyserver.QueryFailedException e) { } catch (Keyserver.QueryFailedException e) {
Log.e(Constants.TAG, "query failed", e); Log.d(Constants.TAG, "query failed", e);
log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage()); log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER, 3, e.getMessage());
} }
} }
@@ -264,7 +243,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
} catch (Keyserver.QueryFailedException e) { } catch (Keyserver.QueryFailedException e) {
// download failed, too bad. just proceed // download failed, too bad. just proceed
Log.e(Constants.TAG, "query failed", e); Log.e(Constants.TAG, "query failed", e);
log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage()); log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER, 3, e.getMessage());
} }
} }
} }
@@ -275,15 +254,11 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
continue; continue;
} }
// If we have an expected fingerprint, make sure it matches // never import secret keys from keyserver!
if (entry.mExpectedFingerprint != null) { if (entry.mBytes == null && key.isSecret()) {
if (!key.containsSubkey(entry.mExpectedFingerprint)) { log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER_SECRET, 2);
log.add(LogType.MSG_IMPORT_FINGERPRINT_ERROR, 2); badKeys += 1;
badKeys += 1; continue;
continue;
} else {
log.add(LogType.MSG_IMPORT_FINGERPRINT_OK, 2);
}
} }
// Another check if we have been cancelled // Another check if we have been cancelled
@@ -293,31 +268,44 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
} }
SaveKeyringResult result; SaveKeyringResult result;
mProviderHelper.clearLog(); // synchronizing prevents https://github.com/open-keychain/open-keychain/issues/1221
if (key.isSecret()) { // and https://github.com/open-keychain/open-keychain/issues/1480
result = mProviderHelper.saveSecretKeyRing(key, synchronized (mProviderHelper) {
new ProgressScaler(progressable, (int) (position * progSteps), mProviderHelper.clearLog();
(int) ((position + 1) * progSteps), 100)); if (key.isSecret()) {
} else { result = mProviderHelper.saveSecretKeyRing(key,
result = mProviderHelper.savePublicKeyRing(key, new ProgressScaler(progressable, (int) (position * progSteps),
new ProgressScaler(progressable, (int) (position * progSteps), (int) ((position + 1) * progSteps), 100));
(int) ((position + 1) * progSteps), 100)); } else {
result = mProviderHelper.savePublicKeyRing(key,
new ProgressScaler(progressable, (int) (position * progSteps),
(int) ((position + 1) * progSteps), 100), entry.mExpectedFingerprint);
}
} }
if (!result.success()) { if (!result.success()) {
badKeys += 1; badKeys += 1;
} else if (result.updated()) {
updatedKeys += 1;
importedMasterKeyIds.add(key.getMasterKeyId());
} else { } else {
newKeys += 1; if (result.updated()) {
if (key.isSecret()) { updatedKeys += 1;
secret += 1; importedMasterKeyIds.add(key.getMasterKeyId());
} else {
newKeys += 1;
if (key.isSecret()) {
secret += 1;
}
importedMasterKeyIds.add(key.getMasterKeyId());
}
if (entry.mBytes == null) {
// synonymous to isDownloadFromKeyserver.
// If no byte data was supplied, import from keyserver took place
// this prevents file imports being noted as keyserver imports
mProviderHelper.renewKeyLastUpdatedTime(key.getMasterKeyId(),
GregorianCalendar.getInstance().getTimeInMillis(),
TimeUnit.MILLISECONDS);
} }
importedMasterKeyIds.add(key.getMasterKeyId());
} }
log.add(result, 2); log.add(result, 2);
} catch (IOException | PgpGeneralException e) { } catch (IOException | PgpGeneralException e) {
Log.e(Constants.TAG, "Encountered bad key on import!", e); Log.e(Constants.TAG, "Encountered bad key on import!", e);
++badKeys; ++badKeys;
@@ -327,9 +315,15 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
} }
// Special: consolidate on secret key import (cannot be cancelled!) // Special: consolidate on secret key import (cannot be cancelled!)
// synchronized on mProviderHelper to prevent
// https://github.com/open-keychain/open-keychain/issues/1221 since a consolidate deletes
// and re-inserts keys, which could conflict with a parallel db key update
if (secret > 0) { if (secret > 0) {
setPreventCancel(); setPreventCancel();
ConsolidateResult result = mProviderHelper.consolidateDatabaseStep1(progressable); ConsolidateResult result;
synchronized (mProviderHelper) {
result = mProviderHelper.consolidateDatabaseStep1(progressable);
}
log.add(result, 1); log.add(result, 1);
} }
@@ -386,7 +380,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
@NonNull @NonNull
@Override @Override
public OperationResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) { public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) {
ArrayList<ParcelableKeyRing> keyList = importInput.mKeyList; ArrayList<ParcelableKeyRing> keyList = importInput.mKeyList;
String keyServer = importInput.mKeyserver; String keyServer = importInput.mKeyserver;
@@ -411,20 +405,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
} else { } else {
proxy = cryptoInput.getParcelableProxy().getProxy(); proxy = cryptoInput.getParcelableProxy().getProxy();
} }
// if there is more than one key with the same fingerprint, we do a serial import to
// prevent result = multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer, proxy);
// https://github.com/open-keychain/open-keychain/issues/1221
HashSet<String> keyFingerprintSet = new HashSet<>();
for (int i = 0; i < keyList.size(); i++) {
keyFingerprintSet.add(keyList.get(i).mExpectedFingerprint);
}
if (keyFingerprintSet.size() == keyList.size()) {
// all keys have unique fingerprints
result = multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer,
proxy);
} else {
result = serialKeyRingImport(keyList, keyServer, proxy);
}
} }
ContactSyncAdapterService.requestSync(); ContactSyncAdapterService.requestSync();
@@ -462,7 +444,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
ArrayList<ParcelableKeyRing> list = new ArrayList<>(); ArrayList<ParcelableKeyRing> list = new ArrayList<>();
list.add(pkRing); list.add(pkRing);
return serialKeyRingImport(list, keyServer, ignoreProgressable, proxy); return serialKeyRingImport(list.iterator(), 1, keyServer,
ignoreProgressable, proxy);
} }
}; };
@@ -486,18 +469,18 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
} }
return accumulator.getConsolidatedResult(); return accumulator.getConsolidatedResult();
} }
return null; // TODO: Decide if we should just crash instead of returning null return new ImportKeyResult(ImportKeyResult.RESULT_FAIL_NOTHING, new OperationLog());
} }
/** /**
* Used to accumulate the results of individual key imports * Used to accumulate the results of individual key imports
*/ */
private class KeyImportAccumulator { public static class KeyImportAccumulator {
private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog(); private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog();
Progressable mProgressable; Progressable mProgressable;
private int mTotalKeys; private int mTotalKeys;
private int mImportedKeys = 0; private int mImportedKeys = 0;
ArrayList<Long> mImportedMasterKeyIds = new ArrayList<Long>(); ArrayList<Long> mImportedMasterKeyIds = new ArrayList<>();
private int mBadKeys = 0; private int mBadKeys = 0;
private int mNewKeys = 0; private int mNewKeys = 0;
private int mUpdatedKeys = 0; private int mUpdatedKeys = 0;
@@ -515,21 +498,17 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
public KeyImportAccumulator(int totalKeys, Progressable externalProgressable) { public KeyImportAccumulator(int totalKeys, Progressable externalProgressable) {
mTotalKeys = totalKeys; mTotalKeys = totalKeys;
mProgressable = externalProgressable; mProgressable = externalProgressable;
mProgressable.setProgress(0, totalKeys); if (mProgressable != null) {
} mProgressable.setProgress(0, totalKeys);
}
public int getTotalKeys() {
return mTotalKeys;
}
public int getImportedKeys() {
return mImportedKeys;
} }
public synchronized void accumulateKeyImport(ImportKeyResult result) { public synchronized void accumulateKeyImport(ImportKeyResult result) {
mImportedKeys++; mImportedKeys++;
mProgressable.setProgress(mImportedKeys, mTotalKeys); if (mProgressable != null) {
mProgressable.setProgress(mImportedKeys, mTotalKeys);
}
mImportLog.addAll(result.getLog().toList());//accumulates log mImportLog.addAll(result.getLog().toList());//accumulates log
mBadKeys += result.mBadKeys; mBadKeys += result.mBadKeys;

View File

@@ -43,7 +43,7 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult; import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
@@ -141,7 +141,7 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat
} }
} }
PgpDecryptVerify op = new PgpDecryptVerify(mContext, mProviderHelper, mProgressable); PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(messageBytes) PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(messageBytes)
.setSignedLiteralData(true) .setSignedLiteralData(true)

View File

@@ -20,7 +20,6 @@
package org.sufficientlysecure.keychain.operations; package org.sufficientlysecure.keychain.operations;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@@ -39,7 +38,7 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> { public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> {
public RevokeOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { public RevokeOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
super(context, providerHelper, progressable); super(context, providerHelper, progressable);
@@ -71,13 +70,15 @@ public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> {
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
} }
SaveKeyringParcel saveKeyringParcel = getRevokedSaveKeyringParcel(masterKeyId, SaveKeyringParcel saveKeyringParcel =
keyRing.getFingerprint()); new SaveKeyringParcel(masterKeyId, keyRing.getFingerprint());
// all revoke operations are made atomic as of now // all revoke operations are made atomic as of now
saveKeyringParcel.setUpdateOptions(revokeKeyringParcel.mUpload, true, saveKeyringParcel.setUpdateOptions(revokeKeyringParcel.mUpload, true,
revokeKeyringParcel.mKeyserver); revokeKeyringParcel.mKeyserver);
saveKeyringParcel.mRevokeSubKeys.add(masterKeyId);
InputPendingResult revokeAndUploadResult = new EditKeyOperation(mContext, InputPendingResult revokeAndUploadResult = new EditKeyOperation(mContext,
mProviderHelper, mProgressable, mCancelled) mProviderHelper, mProgressable, mCancelled)
.execute(saveKeyringParcel, cryptoInputParcel); .execute(saveKeyringParcel, cryptoInputParcel);
@@ -92,54 +93,15 @@ public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> {
log.add(OperationResult.LogType.MSG_REVOKE_OK, 1); log.add(OperationResult.LogType.MSG_REVOKE_OK, 1);
return new RevokeResult(RevokeResult.RESULT_OK, log, masterKeyId); return new RevokeResult(RevokeResult.RESULT_OK, log, masterKeyId);
} else { } else {
log.add(OperationResult.LogType.MSG_REVOKE_KEY_FAIL, 1); log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1);
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
} }
} catch (PgpKeyNotFoundException | ProviderHelper.NotFoundException e) { } catch (PgpKeyNotFoundException | ProviderHelper.NotFoundException e) {
Log.e(Constants.TAG, "could not find key to revoke", e); Log.e(Constants.TAG, "could not find key to revoke", e);
log.add(OperationResult.LogType.MSG_REVOKE_KEY_FAIL, 1); log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1);
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
} }
} }
private SaveKeyringParcel getRevokedSaveKeyringParcel(long masterKeyId, byte[] fingerprint) {
final String[] SUBKEYS_PROJECTION = new String[]{
KeychainContract.Keys.KEY_ID
};
final int INDEX_KEY_ID = 0;
Uri keysUri = KeychainContract.Keys.buildKeysUri(masterKeyId);
Cursor subKeyCursor =
mContext.getContentResolver().query(keysUri, SUBKEYS_PROJECTION, null, null, null);
SaveKeyringParcel saveKeyringParcel =
new SaveKeyringParcel(masterKeyId, fingerprint);
// add all subkeys, for revocation
while (subKeyCursor != null && subKeyCursor.moveToNext()) {
saveKeyringParcel.mRevokeSubKeys.add(subKeyCursor.getLong(INDEX_KEY_ID));
}
if (subKeyCursor != null) {
subKeyCursor.close();
}
final String[] USER_IDS_PROJECTION = new String[]{
KeychainContract.UserPackets.USER_ID
};
final int INDEX_USER_ID = 0;
Uri userIdsUri = KeychainContract.UserPackets.buildUserIdsUri(masterKeyId);
Cursor userIdCursor = mContext.getContentResolver().query(
userIdsUri, USER_IDS_PROJECTION, null, null, null);
while (userIdCursor != null && userIdCursor.moveToNext()) {
saveKeyringParcel.mRevokeUserIds.add(userIdCursor.getString(INDEX_USER_ID));
}
if (userIdCursor != null) {
userIdCursor.close();
}
return saveKeyringParcel;
}
} }

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.operations.results;
import android.os.Parcel;
public class LinkedVerifyResult extends OperationResult {
public LinkedVerifyResult(int result, OperationLog log) {
super(result, log);
}
/** Construct from a parcel - trivial because we have no extra data. */
public LinkedVerifyResult(Parcel source) {
super(source);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
}
public static Creator<LinkedVerifyResult> CREATOR = new Creator<LinkedVerifyResult>() {
public LinkedVerifyResult createFromParcel(final Parcel source) {
return new LinkedVerifyResult(source);
}
public LinkedVerifyResult[] newArray(final int size) {
return new LinkedVerifyResult[size];
}
};
}

View File

@@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.operations.results;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.res.Resources;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@@ -52,6 +53,8 @@ import java.util.List;
*/ */
public abstract class OperationResult implements Parcelable { public abstract class OperationResult implements Parcelable {
final static String INDENTATION_WHITESPACE = " ";
public static final String EXTRA_RESULT = "operation_result"; public static final String EXTRA_RESULT = "operation_result";
/** /**
@@ -166,6 +169,27 @@ public abstract class OperationResult implements Parcelable {
", mIndent=" + mIndent + ", mIndent=" + mIndent +
'}'; '}';
} }
StringBuilder getPrintableLogEntry(Resources resources, int indent) {
StringBuilder result = new StringBuilder();
int padding = mIndent +indent;
if (padding > INDENTATION_WHITESPACE.length()) {
padding = INDENTATION_WHITESPACE.length();
}
result.append(INDENTATION_WHITESPACE, 0, padding);
result.append(LOG_LEVEL_NAME[mType.mLevel.ordinal()]).append(' ');
// special case: first parameter may be a quantity
if (mParameters != null && mParameters.length > 0 && mParameters[0] instanceof Integer) {
result.append(resources.getQuantityString(mType.getMsgId(), (Integer) mParameters[0], mParameters));
} else {
result.append(resources.getString(mType.getMsgId(), mParameters));
}
return result;
}
} }
public static class SubLogEntryParcel extends LogEntryParcel { public static class SubLogEntryParcel extends LogEntryParcel {
@@ -202,6 +226,17 @@ public abstract class OperationResult implements Parcelable {
dest.writeParcelable(mSubResult, 0); dest.writeParcelable(mSubResult, 0);
} }
@Override
StringBuilder getPrintableLogEntry(Resources resources, int indent) {
LogEntryParcel subEntry = mSubResult.getLog().getLast();
if (subEntry != null) {
return subEntry.getPrintableLogEntry(resources, mIndent +indent);
} else {
return super.getPrintableLogEntry(resources, indent);
}
}
} }
public Showable createNotify(final Activity activity) { public Showable createNotify(final Activity activity) {
@@ -245,15 +280,15 @@ public abstract class OperationResult implements Parcelable {
} }
return Notify.create(activity, logText, Notify.LENGTH_LONG, style, return Notify.create(activity, logText, Notify.LENGTH_LONG, style,
new ActionListener() { new ActionListener() {
@Override @Override
public void onAction() { public void onAction() {
Intent intent = new Intent( Intent intent = new Intent(
activity, LogDisplayActivity.class); activity, LogDisplayActivity.class);
intent.putExtra(LogDisplayFragment.EXTRA_RESULT, OperationResult.this); intent.putExtra(LogDisplayFragment.EXTRA_RESULT, OperationResult.this);
activity.startActivity(intent); activity.startActivity(intent);
} }
}, R.string.snackbar_details); }, R.string.snackbar_details);
} }
@@ -289,6 +324,8 @@ public abstract class OperationResult implements Parcelable {
MSG_IP_ERROR_IO_EXC (LogLevel.ERROR, R.string.msg_ip_error_io_exc), MSG_IP_ERROR_IO_EXC (LogLevel.ERROR, R.string.msg_ip_error_io_exc),
MSG_IP_ERROR_OP_EXC (LogLevel.ERROR, R.string.msg_ip_error_op_exc), MSG_IP_ERROR_OP_EXC (LogLevel.ERROR, R.string.msg_ip_error_op_exc),
MSG_IP_ERROR_REMOTE_EX (LogLevel.ERROR, R.string.msg_ip_error_remote_ex), MSG_IP_ERROR_REMOTE_EX (LogLevel.ERROR, R.string.msg_ip_error_remote_ex),
MSG_IP_FINGERPRINT_ERROR (LogLevel.ERROR, R.string.msg_ip_fingerprint_error),
MSG_IP_FINGERPRINT_OK (LogLevel.INFO, R.string.msg_ip_fingerprint_ok),
MSG_IP_INSERT_KEYRING (LogLevel.DEBUG, R.string.msg_ip_insert_keyring), MSG_IP_INSERT_KEYRING (LogLevel.DEBUG, R.string.msg_ip_insert_keyring),
MSG_IP_INSERT_SUBKEYS (LogLevel.DEBUG, R.string.msg_ip_insert_keys), MSG_IP_INSERT_SUBKEYS (LogLevel.DEBUG, R.string.msg_ip_insert_keys),
MSG_IP_PREPARE (LogLevel.DEBUG, R.string.msg_ip_prepare), MSG_IP_PREPARE (LogLevel.DEBUG, R.string.msg_ip_prepare),
@@ -631,6 +668,7 @@ public abstract class OperationResult implements Parcelable {
MSG_DC_TRAIL_SYM (LogLevel.DEBUG, R.string.msg_dc_trail_sym), MSG_DC_TRAIL_SYM (LogLevel.DEBUG, R.string.msg_dc_trail_sym),
MSG_DC_TRAIL_UNKNOWN (LogLevel.DEBUG, R.string.msg_dc_trail_unknown), MSG_DC_TRAIL_UNKNOWN (LogLevel.DEBUG, R.string.msg_dc_trail_unknown),
MSG_DC_UNLOCKING (LogLevel.INFO, R.string.msg_dc_unlocking), MSG_DC_UNLOCKING (LogLevel.INFO, R.string.msg_dc_unlocking),
MSG_DC_INSECURE_ENCRYPTION_KEY (LogLevel.WARN, R.string.msg_dc_insecure_encryption_key),
MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO(LogLevel.WARN, R.string.msg_dc_insecure_symmetric_encryption_algo), MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO(LogLevel.WARN, R.string.msg_dc_insecure_symmetric_encryption_algo),
MSG_DC_INSECURE_HASH_ALGO(LogLevel.ERROR, R.string.msg_dc_insecure_hash_algo), MSG_DC_INSECURE_HASH_ALGO(LogLevel.ERROR, R.string.msg_dc_insecure_hash_algo),
MSG_DC_INSECURE_MDC_MISSING(LogLevel.ERROR, R.string.msg_dc_insecure_mdc_missing), MSG_DC_INSECURE_MDC_MISSING(LogLevel.ERROR, R.string.msg_dc_insecure_mdc_missing),
@@ -704,21 +742,21 @@ public abstract class OperationResult implements Parcelable {
MSG_IMPORT_FETCH_ERROR (LogLevel.ERROR, R.string.msg_import_fetch_error), MSG_IMPORT_FETCH_ERROR (LogLevel.ERROR, R.string.msg_import_fetch_error),
MSG_IMPORT_FETCH_ERROR_DECODE (LogLevel.ERROR, R.string.msg_import_fetch_error_decode), MSG_IMPORT_FETCH_ERROR_DECODE (LogLevel.ERROR, R.string.msg_import_fetch_error_decode),
MSG_IMPORT_FETCH_ERROR_KEYSERVER(LogLevel.ERROR, R.string.msg_import_fetch_error_keyserver),
MSG_IMPORT_FETCH_ERROR_KEYSERVER_SECRET (LogLevel.ERROR, R.string.msg_import_fetch_error_keyserver_secret),
MSG_IMPORT_FETCH_KEYBASE (LogLevel.INFO, R.string.msg_import_fetch_keybase),
MSG_IMPORT_FETCH_KEYSERVER (LogLevel.INFO, R.string.msg_import_fetch_keyserver), MSG_IMPORT_FETCH_KEYSERVER (LogLevel.INFO, R.string.msg_import_fetch_keyserver),
MSG_IMPORT_FETCH_KEYSERVER_OK (LogLevel.DEBUG, R.string.msg_import_fetch_keyserver_ok), MSG_IMPORT_FETCH_KEYSERVER_OK (LogLevel.DEBUG, R.string.msg_import_fetch_keyserver_ok),
MSG_IMPORT_FETCH_KEYSERVER_ERROR (LogLevel.ERROR, R.string.msg_import_fetch_keyserver_error),
MSG_IMPORT_FETCH_KEYBASE (LogLevel.INFO, R.string.msg_import_fetch_keybase),
MSG_IMPORT_KEYSERVER (LogLevel.DEBUG, R.string.msg_import_keyserver), MSG_IMPORT_KEYSERVER (LogLevel.DEBUG, R.string.msg_import_keyserver),
MSG_IMPORT_MERGE (LogLevel.DEBUG, R.string.msg_import_merge), MSG_IMPORT_MERGE (LogLevel.DEBUG, R.string.msg_import_merge),
MSG_IMPORT_MERGE_ERROR (LogLevel.ERROR, R.string.msg_import_merge_error), MSG_IMPORT_MERGE_ERROR (LogLevel.ERROR, R.string.msg_import_merge_error),
MSG_IMPORT_FINGERPRINT_ERROR (LogLevel.ERROR, R.string.msg_import_fingerprint_error),
MSG_IMPORT_FINGERPRINT_OK (LogLevel.DEBUG, R.string.msg_import_fingerprint_ok),
MSG_IMPORT_ERROR (LogLevel.ERROR, R.string.msg_import_error), MSG_IMPORT_ERROR (LogLevel.ERROR, R.string.msg_import_error),
MSG_IMPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_import_error_io), MSG_IMPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_import_error_io),
MSG_IMPORT_PARTIAL (LogLevel.ERROR, R.string.msg_import_partial), MSG_IMPORT_PARTIAL (LogLevel.ERROR, R.string.msg_import_partial),
MSG_IMPORT_SUCCESS (LogLevel.OK, R.string.msg_import_success), MSG_IMPORT_SUCCESS (LogLevel.OK, R.string.msg_import_success),
MSG_EXPORT (LogLevel.START, R.plurals.msg_export), MSG_EXPORT (LogLevel.START, R.plurals.msg_export),
MSG_EXPORT_FILE_NAME (LogLevel.INFO, R.string.msg_export_file_name),
MSG_EXPORT_UPLOAD_PUBLIC (LogLevel.START, R.string.msg_export_upload_public), MSG_EXPORT_UPLOAD_PUBLIC (LogLevel.START, R.string.msg_export_upload_public),
MSG_EXPORT_PUBLIC (LogLevel.DEBUG, R.string.msg_export_public), MSG_EXPORT_PUBLIC (LogLevel.DEBUG, R.string.msg_export_public),
MSG_EXPORT_SECRET (LogLevel.DEBUG, R.string.msg_export_secret), MSG_EXPORT_SECRET (LogLevel.DEBUG, R.string.msg_export_secret),
@@ -763,10 +801,9 @@ public abstract class OperationResult implements Parcelable {
MSG_DEL_FAIL (LogLevel.WARN, R.plurals.msg_del_fail), MSG_DEL_FAIL (LogLevel.WARN, R.plurals.msg_del_fail),
MSG_REVOKE_ERROR_EMPTY (LogLevel.ERROR, R.string.msg_revoke_error_empty), MSG_REVOKE_ERROR_EMPTY (LogLevel.ERROR, R.string.msg_revoke_error_empty),
MSG_REVOKE_ERROR_MULTI_SECRET (LogLevel.DEBUG, R.string.msg_revoke_error_multi_secret), MSG_REVOKE_ERROR_NOT_FOUND (LogLevel.ERROR, R.string.msg_revoke_error_not_found),
MSG_REVOKE_ERROR_NOT_FOUND (LogLevel.DEBUG, R.string.msg_revoke_error_multi_secret),
MSG_REVOKE (LogLevel.DEBUG, R.string.msg_revoke_key), MSG_REVOKE (LogLevel.DEBUG, R.string.msg_revoke_key),
MSG_REVOKE_KEY_FAIL (LogLevel.ERROR, R.string.msg_revoke_key_fail), MSG_REVOKE_ERROR_KEY_FAIL (LogLevel.ERROR, R.string.msg_revoke_key_fail),
MSG_REVOKE_OK (LogLevel.OK, R.string.msg_revoke_ok), MSG_REVOKE_OK (LogLevel.OK, R.string.msg_revoke_ok),
// keybase verification // keybase verification
@@ -781,17 +818,31 @@ public abstract class OperationResult implements Parcelable {
MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH(LogLevel.ERROR, MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH(LogLevel.ERROR,
R.string.msg_keybase_error_msg_payload_mismatch), R.string.msg_keybase_error_msg_payload_mismatch),
// export log // mime parsing
MSG_EXPORT_LOG(LogLevel.START,R.string.msg_export_log_start),
MSG_EXPORT_LOG_EXPORT_ERROR_NO_FILE(LogLevel.ERROR,R.string.msg_export_log_error_no_file),
MSG_EXPORT_LOG_EXPORT_ERROR_FOPEN(LogLevel.ERROR,R.string.msg_export_log_error_fopen),
MSG_EXPORT_LOG_EXPORT_ERROR_WRITING(LogLevel.ERROR,R.string.msg_export_log_error_writing),
MSG_EXPORT_LOG_EXPORT_SUCCESS (LogLevel.OK, R.string.msg_export_log_success),
// mim parsing
MSG_MIME_PARSING(LogLevel.START,R.string.msg_mime_parsing_start), MSG_MIME_PARSING(LogLevel.START,R.string.msg_mime_parsing_start),
MSG_MIME_PARSING_ERROR(LogLevel.ERROR,R.string.msg_mime_parsing_error), MSG_MIME_PARSING_ERROR(LogLevel.ERROR,R.string.msg_mime_parsing_error),
MSG_MIME_PARSING_SUCCESS(LogLevel.OK,R.string.msg_mime_parsing_success), MSG_MIME_PARSING_SUCCESS(LogLevel.OK,R.string.msg_mime_parsing_success),
MSG_LV (LogLevel.START, R.string.msg_lv),
MSG_LV_MATCH (LogLevel.DEBUG, R.string.msg_lv_match),
MSG_LV_MATCH_ERROR (LogLevel.ERROR, R.string.msg_lv_match_error),
MSG_LV_FP_OK (LogLevel.DEBUG, R.string.msg_lv_fp_ok),
MSG_LV_FP_ERROR (LogLevel.ERROR, R.string.msg_lv_fp_error),
MSG_LV_ERROR_TWITTER_AUTH (LogLevel.ERROR, R.string.msg_lv_error_twitter_auth),
MSG_LV_ERROR_TWITTER_HANDLE (LogLevel.ERROR, R.string.msg_lv_error_twitter_handle),
MSG_LV_ERROR_TWITTER_RESPONSE (LogLevel.ERROR, R.string.msg_lv_error_twitter_response),
MSG_LV_ERROR_GITHUB_HANDLE (LogLevel.ERROR, R.string.msg_lv_error_github_handle),
MSG_LV_ERROR_GITHUB_NOT_FOUND (LogLevel.ERROR, R.string.msg_lv_error_github_not_found),
MSG_LV_FETCH (LogLevel.DEBUG, R.string.msg_lv_fetch),
MSG_LV_FETCH_REDIR (LogLevel.DEBUG, R.string.msg_lv_fetch_redir),
MSG_LV_FETCH_OK (LogLevel.DEBUG, R.string.msg_lv_fetch_ok),
MSG_LV_FETCH_ERROR (LogLevel.ERROR, R.string.msg_lv_fetch_error),
MSG_LV_FETCH_ERROR_URL (LogLevel.ERROR, R.string.msg_lv_fetch_error_url),
MSG_LV_FETCH_ERROR_IO (LogLevel.ERROR, R.string.msg_lv_fetch_error_io),
MSG_LV_FETCH_ERROR_FORMAT(LogLevel.ERROR, R.string.msg_lv_fetch_error_format),
MSG_LV_FETCH_ERROR_NOTHING (LogLevel.ERROR, R.string.msg_lv_fetch_error_nothing),
; ;
public final int mMsgId; public final int mMsgId;
@@ -815,6 +866,10 @@ public abstract class OperationResult implements Parcelable {
OK, // should occur once at the end of a successful operation OK, // should occur once at the end of a successful operation
CANCELLED, // should occur once at the end of a cancelled operation CANCELLED, // should occur once at the end of a cancelled operation
} }
// for print of debug log. keep those in sync with above!
static final String[] LOG_LEVEL_NAME = new String[] {
"[DEBUG]", "[INFO]", "[WARN]", "[ERROR]", "[START]", "[OK]", "[CANCEL]"
};
@Override @Override
public int describeContents() { public int describeContents() {
@@ -913,6 +968,20 @@ public abstract class OperationResult implements Parcelable {
public Iterator<LogEntryParcel> iterator() { public Iterator<LogEntryParcel> iterator() {
return mParcels.iterator(); return mParcels.iterator();
} }
/**
* returns an indented String of an entire OperationLog
* @param indent padding to add at the start of all log entries, made for use with SubLogs
* @return printable, indented version of passed operationLog
*/
public String getPrintableOperationLog(Resources resources, int indent) {
StringBuilder log = new StringBuilder();
for (LogEntryParcel entry : this) {
log.append(entry.getPrintableLogEntry(resources, indent)).append("\n");
}
return log.toString().substring(0, log.length() -1); // get rid of extra new line
}
} }
} }

View File

@@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.pgp;
import org.spongycastle.openpgp.PGPKeyRing; import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKey;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.IterableIterator;
import java.io.IOException; import java.io.IOException;
@@ -28,6 +29,7 @@ import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.Set; import java.util.Set;
@@ -152,4 +154,14 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
return getRing().getEncoded(); return getRing().getEncoded();
} }
public boolean containsSubkey(String expectedFingerprint) {
for (CanonicalizedPublicKey key : publicKeyIterator()) {
if (KeyFormattingUtils.convertFingerprintToHex(
key.getFingerprint()).equalsIgnoreCase(expectedFingerprint)) {
return true;
}
}
return false;
}
} }

View File

@@ -79,9 +79,9 @@ import java.security.SignatureException;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> { public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInputParcel> {
public PgpDecryptVerify(Context context, ProviderHelper providerHelper, Progressable progressable) { public PgpDecryptVerifyOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
super(context, providerHelper, progressable); super(context, providerHelper, progressable);
} }
@@ -313,6 +313,27 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
return result; return result;
} }
private static class EncryptStreamResult {
// this is non-null iff an error occured, return directly
DecryptVerifyResult errorResult;
// for verification
PGPEncryptedData encryptedData;
InputStream cleartextStream;
int symmetricEncryptionAlgo = 0;
boolean skippedDisallowedKey = false;
boolean insecureEncryptionKey = false;
// convenience method to return with error
public EncryptStreamResult with(DecryptVerifyResult result) {
errorResult = result;
return this;
}
}
/** Decrypt and/or verify binary or ascii armored pgp data. */ /** Decrypt and/or verify binary or ascii armored pgp data. */
@NonNull @NonNull
@@ -320,42 +341,14 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput, PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput,
InputStream in, OutputStream out, int indent) throws IOException, PGPException { InputStream in, OutputStream out, int indent) throws IOException, PGPException {
OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder();
OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder();
OperationLog log = new OperationLog(); OperationLog log = new OperationLog();
log.add(LogType.MSG_DC, indent); log.add(LogType.MSG_DC, indent);
indent += 1; indent += 1;
JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
PGPEncryptedDataList enc;
Object o = pgpF.nextObject();
int currentProgress = 0; int currentProgress = 0;
updateProgress(R.string.progress_reading_data, currentProgress, 100); updateProgress(R.string.progress_reading_data, currentProgress, 100);
if (o instanceof PGPEncryptedDataList) {
enc = (PGPEncryptedDataList) o;
} else {
enc = (PGPEncryptedDataList) pgpF.nextObject();
}
if (enc == null) {
log.add(LogType.MSG_DC_ERROR_INVALID_DATA, indent);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
InputStream clear;
PGPEncryptedData encryptedData;
PGPPublicKeyEncryptedData encryptedDataAsymmetric = null;
PGPPBEEncryptedData encryptedDataSymmetric = null;
CanonicalizedSecretKey secretEncryptionKey = null;
Iterator<?> it = enc.getEncryptedDataObjects();
boolean asymmetricPacketFound = false;
boolean symmetricPacketFound = false;
boolean anyPacketFound = false;
// If the input stream is armored, and there is a charset specified, take a note for later // If the input stream is armored, and there is a charset specified, take a note for later
// https://tools.ietf.org/html/rfc4880#page56 // https://tools.ietf.org/html/rfc4880#page56
String charset = null; String charset = null;
@@ -375,261 +368,53 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
} }
} }
Passphrase passphrase = null; OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder();
boolean skippedDisallowedKey = false; OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder();
// go through all objects and find one we can decrypt JcaPGPObjectFactory plainFact;
while (it.hasNext()) { Object dataChunk;
Object obj = it.next(); EncryptStreamResult esResult = null;
if (obj instanceof PGPPublicKeyEncryptedData) { { // resolve encrypted (symmetric and asymmetric) packets
anyPacketFound = true; JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
Object obj = pgpF.nextObject();
currentProgress += 2; if (obj instanceof PGPEncryptedDataList) {
updateProgress(R.string.progress_finding_key, currentProgress, 100); esResult = handleEncryptedPacket(
input, cryptoInput, (PGPEncryptedDataList) obj, log, indent, currentProgress);
PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj; // if there is an error, nothing left to do here
long subKeyId = encData.getKeyID(); if (esResult.errorResult != null) {
return esResult.errorResult;
log.add(LogType.MSG_DC_ASYM, indent,
KeyFormattingUtils.convertKeyIdToHex(subKeyId));
CanonicalizedSecretKeyRing secretKeyRing;
try {
// get actual keyring object based on master key id
secretKeyRing = mProviderHelper.getCanonicalizedSecretKeyRing(
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId)
);
} catch (ProviderHelper.NotFoundException e) {
// continue with the next packet in the while loop
log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1);
continue;
}
if (secretKeyRing == null) {
// continue with the next packet in the while loop
log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1);
continue;
} }
// allow only specific keys for decryption? // if this worked out so far, the data is encrypted
if (input.getAllowedKeyIds() != null) { decryptionResultBuilder.setEncrypted(true);
long masterKeyId = secretKeyRing.getMasterKeyId();
Log.d(Constants.TAG, "encData.getKeyID(): " + subKeyId);
Log.d(Constants.TAG, "mAllowedKeyIds: " + input.getAllowedKeyIds());
Log.d(Constants.TAG, "masterKeyId: " + masterKeyId);
if (!input.getAllowedKeyIds().contains(masterKeyId)) { if (esResult.insecureEncryptionKey) {
// this key is in our db, but NOT allowed! log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1);
// continue with the next packet in the while loop
skippedDisallowedKey = true;
log.add(LogType.MSG_DC_ASKIP_NOT_ALLOWED, indent + 1);
continue;
}
}
// get subkey which has been used for this encryption packet
secretEncryptionKey = secretKeyRing.getSecretKey(subKeyId);
if (secretEncryptionKey == null) {
// should actually never happen, so no need to be more specific.
log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1);
continue;
}
/* secret key exists in database and is allowed! */
asymmetricPacketFound = true;
encryptedDataAsymmetric = encData;
if (secretEncryptionKey.getSecretKeyType() == SecretKeyType.DIVERT_TO_CARD) {
passphrase = null;
} else if (cryptoInput.hasPassphrase()) {
passphrase = cryptoInput.getPassphrase();
} else {
// if no passphrase was explicitly set try to get it from the cache service
try {
// returns "" if key has no passphrase
passphrase = getCachedPassphrase(subKeyId);
log.add(LogType.MSG_DC_PASS_CACHED, indent + 1);
} catch (PassphraseCacheInterface.NoSecretKeyException e) {
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
// if passphrase was not cached, return here indicating that a passphrase is missing!
if (passphrase == null) {
log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1);
return new DecryptVerifyResult(log,
RequiredInputParcel.createRequiredDecryptPassphrase(
secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId()),
cryptoInput);
}
}
// check for insecure encryption key
if ( ! PgpSecurityConstants.isSecureKey(secretEncryptionKey)) {
log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1);
decryptionResultBuilder.setInsecure(true); decryptionResultBuilder.setInsecure(true);
} }
// break out of while, only decrypt the first packet where we have a key // Check for insecure encryption algorithms!
break; if (!PgpSecurityConstants.isSecureSymmetricAlgorithm(esResult.symmetricEncryptionAlgo)) {
log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1);
} else if (obj instanceof PGPPBEEncryptedData) { decryptionResultBuilder.setInsecure(true);
anyPacketFound = true;
log.add(LogType.MSG_DC_SYM, indent);
if (!input.isAllowSymmetricDecryption()) {
log.add(LogType.MSG_DC_SYM_SKIP, indent + 1);
continue;
} }
/* plainFact = new JcaPGPObjectFactory(esResult.cleartextStream);
* When mAllowSymmetricDecryption == true and we find a data packet here, dataChunk = plainFact.nextObject();
* we do not search for other available asymmetric packets!
*/
symmetricPacketFound = true;
encryptedDataSymmetric = (PGPPBEEncryptedData) obj;
// if no passphrase is given, return here
// indicating that a passphrase is missing!
if (!cryptoInput.hasPassphrase()) {
try {
passphrase = getCachedPassphrase(key.symmetric);
log.add(LogType.MSG_DC_PASS_CACHED, indent + 1);
} catch (PassphraseCacheInterface.NoSecretKeyException e) {
// nvm
}
if (passphrase == null) {
log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1);
return new DecryptVerifyResult(log,
RequiredInputParcel.createRequiredSymmetricPassphrase(),
cryptoInput);
}
} else {
passphrase = cryptoInput.getPassphrase();
}
// break out of while, only decrypt the first packet
break;
}
}
// More data, just acknowledge and ignore.
while (it.hasNext()) {
Object obj = it.next();
if (obj instanceof PGPPublicKeyEncryptedData) {
PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj;
long subKeyId = encData.getKeyID();
log.add(LogType.MSG_DC_TRAIL_ASYM, indent,
KeyFormattingUtils.convertKeyIdToHex(subKeyId));
} else if (obj instanceof PGPPBEEncryptedData) {
log.add(LogType.MSG_DC_TRAIL_SYM, indent);
} else { } else {
log.add(LogType.MSG_DC_TRAIL_UNKNOWN, indent); decryptionResultBuilder.setEncrypted(false);
plainFact = pgpF;
dataChunk = obj;
} }
} }
log.add(LogType.MSG_DC_PREP_STREAMS, indent); log.add(LogType.MSG_DC_PREP_STREAMS, indent);
// we made sure above one of these two would be true
int symmetricEncryptionAlgo;
if (symmetricPacketFound) {
currentProgress += 2;
updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
PGPDigestCalculatorProvider digestCalcProvider = new JcaPGPDigestCalculatorProviderBuilder()
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build();
PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(
digestCalcProvider).setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
passphrase.getCharArray());
try {
clear = encryptedDataSymmetric.getDataStream(decryptorFactory);
} catch (PGPDataValidationException e) {
log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent +1);
return new DecryptVerifyResult(log,
RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput);
}
encryptedData = encryptedDataSymmetric;
symmetricEncryptionAlgo = encryptedDataSymmetric.getSymmetricAlgorithm(decryptorFactory);
} else if (asymmetricPacketFound) {
currentProgress += 2;
updateProgress(R.string.progress_extracting_key, currentProgress, 100);
try {
log.add(LogType.MSG_DC_UNLOCKING, indent + 1);
if (!secretEncryptionKey.unlock(passphrase)) {
log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
} catch (PgpGeneralException e) {
log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
currentProgress += 2;
updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
CachingDataDecryptorFactory decryptorFactory
= secretEncryptionKey.getCachingDecryptorFactory(cryptoInput);
// special case: if the decryptor does not have a session key cached for this encrypted
// data, and can't actually decrypt on its own, return a pending intent
if (!decryptorFactory.canDecrypt()
&& !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
log.add(LogType.MSG_DC_PENDING_NFC, indent + 1);
return new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation(
secretEncryptionKey.getRing().getMasterKeyId(),
secretEncryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0]
),
cryptoInput);
}
try {
clear = encryptedDataAsymmetric.getDataStream(decryptorFactory);
} catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) {
log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys());
encryptedData = encryptedDataAsymmetric;
} else {
// there wasn't even any useful data
if (!anyPacketFound) {
log.add(LogType.MSG_DC_ERROR_NO_DATA, indent + 1);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_NO_DATA, log);
}
// there was data but key wasn't allowed
if (skippedDisallowedKey) {
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_KEY_DISALLOWED, log);
}
// no packet has been found where we have the corresponding secret key in our db
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
decryptionResultBuilder.setEncrypted(true);
// Check for insecure encryption algorithms!
if (!PgpSecurityConstants.isSecureSymmetricAlgorithm(symmetricEncryptionAlgo)) {
log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1);
decryptionResultBuilder.setInsecure(true);
}
JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear);
Object dataChunk = plainFact.nextObject();
int signatureIndex = -1; int signatureIndex = -1;
CanonicalizedPublicKeyRing signingRing = null; CanonicalizedPublicKeyRing signingRing = null;
CanonicalizedPublicKey signingKey = null; CanonicalizedPublicKey signingKey = null;
@@ -637,6 +422,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
log.add(LogType.MSG_DC_CLEAR, indent); log.add(LogType.MSG_DC_CLEAR, indent);
indent += 1; indent += 1;
// resolve compressed data
if (dataChunk instanceof PGPCompressedData) { if (dataChunk instanceof PGPCompressedData) {
log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1); log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1);
currentProgress += 2; currentProgress += 2;
@@ -649,6 +435,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
plainFact = fact; plainFact = fact;
} }
// resolve leading signature data
PGPOnePassSignature signature = null; PGPOnePassSignature signature = null;
if (dataChunk instanceof PGPOnePassSignatureList) { if (dataChunk instanceof PGPOnePassSignatureList) {
log.add(LogType.MSG_DC_CLEAR_SIGNATURE, indent + 1); log.add(LogType.MSG_DC_CLEAR_SIGNATURE, indent + 1);
@@ -710,159 +497,158 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
OpenPgpMetadata metadata; OpenPgpMetadata metadata;
if (dataChunk instanceof PGPLiteralData) { if ( ! (dataChunk instanceof PGPLiteralData)) {
log.add(LogType.MSG_DC_CLEAR_DATA, indent + 1);
indent += 2;
currentProgress += 4;
updateProgress(R.string.progress_decrypting, currentProgress, 100);
PGPLiteralData literalData = (PGPLiteralData) dataChunk; log.add(LogType.MSG_DC_ERROR_INVALID_DATA, indent);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
String originalFilename = literalData.getFileName(); }
String mimeType = null;
if (literalData.getFormat() == PGPLiteralData.TEXT log.add(LogType.MSG_DC_CLEAR_DATA, indent + 1);
|| literalData.getFormat() == PGPLiteralData.UTF8) { indent += 2;
mimeType = "text/plain"; currentProgress += 4;
updateProgress(R.string.progress_decrypting, currentProgress, 100);
PGPLiteralData literalData = (PGPLiteralData) dataChunk;
String originalFilename = literalData.getFileName();
if (originalFilename.contains("/")) {
originalFilename = originalFilename.substring(originalFilename.lastIndexOf('/'));
}
String mimeType = null;
if (literalData.getFormat() == PGPLiteralData.TEXT
|| literalData.getFormat() == PGPLiteralData.UTF8) {
mimeType = "text/plain";
} else {
// try to guess from file ending
String extension = MimeTypeMap.getFileExtensionFromUrl(originalFilename);
if (extension != null) {
MimeTypeMap mime = MimeTypeMap.getSingleton();
mimeType = mime.getMimeTypeFromExtension(extension);
}
if (mimeType == null) {
mimeType = "application/octet-stream";
}
}
if (!"".equals(originalFilename)) {
log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename);
}
log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1,
mimeType);
log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1,
new Date(literalData.getModificationTime().getTime()).toString());
// return here if we want to decrypt the metadata only
if (input.isDecryptMetadataOnly()) {
// this operation skips the entire stream to find the data length!
Long originalSize = literalData.findDataLength();
if (originalSize != null) {
log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1,
Long.toString(originalSize));
} else { } else {
// try to guess from file ending log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1);
String extension = MimeTypeMap.getFileExtensionFromUrl(originalFilename);
if (extension != null) {
MimeTypeMap mime = MimeTypeMap.getSingleton();
mimeType = mime.getMimeTypeFromExtension(extension);
}
if (mimeType == null) {
mimeType = "application/octet-stream";
}
}
if (!"".equals(originalFilename)) {
log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename);
}
log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1,
mimeType);
log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1,
new Date(literalData.getModificationTime().getTime()).toString());
// return here if we want to decrypt the metadata only
if (input.isDecryptMetadataOnly()) {
// this operation skips the entire stream to find the data length!
Long originalSize = literalData.findDataLength();
if (originalSize != null) {
log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1,
Long.toString(originalSize));
} else {
log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1);
}
metadata = new OpenPgpMetadata(
originalFilename,
mimeType,
literalData.getModificationTime().getTime(),
originalSize == null ? 0 : originalSize);
log.add(LogType.MSG_DC_OK_META_ONLY, indent);
DecryptVerifyResult result =
new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
result.setCharset(charset);
result.setDecryptionMetadata(metadata);
return result;
}
int endProgress;
if (signature != null) {
endProgress = 90;
} else if (encryptedData.isIntegrityProtected()) {
endProgress = 95;
} else {
endProgress = 100;
}
ProgressScaler progressScaler =
new ProgressScaler(mProgressable, currentProgress, endProgress, 100);
InputStream dataIn = literalData.getInputStream();
long alreadyWritten = 0;
long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition();
int length;
byte[] buffer = new byte[1 << 16];
while ((length = dataIn.read(buffer)) > 0) {
// Log.d(Constants.TAG, "read bytes: " + length);
if (out != null) {
out.write(buffer, 0, length);
}
// update signature buffer if signature is also present
if (signature != null) {
signature.update(buffer, 0, length);
}
alreadyWritten += length;
if (wholeSize > 0) {
long progress = 100 * alreadyWritten / wholeSize;
// stop at 100% for wrong file sizes...
if (progress > 100) {
progress = 100;
}
progressScaler.setProgress((int) progress, 100);
}
// TODO: slow annealing to fake a progress?
} }
metadata = new OpenPgpMetadata( metadata = new OpenPgpMetadata(
originalFilename, originalFilename,
mimeType, mimeType,
literalData.getModificationTime().getTime(), literalData.getModificationTime().getTime(),
alreadyWritten); originalSize == null ? 0 : originalSize);
if (signature != null) { log.add(LogType.MSG_DC_OK_META_ONLY, indent);
updateProgress(R.string.progress_verifying_signature, 90, 100); DecryptVerifyResult result =
log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
result.setCharset(charset);
PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject(); result.setDecryptionMetadata(metadata);
PGPSignature messageSignature = signatureList.get(signatureIndex); return result;
// TODO: what about binary signatures?
// Verify signature
boolean validSignature = signature.verify(messageSignature);
if (validSignature) {
log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1);
} else {
log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1);
}
// check for insecure hash algorithms
if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) {
log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1);
signatureResultBuilder.setInsecure(true);
}
signatureResultBuilder.setValidSignature(validSignature);
}
indent -= 1;
} else {
// If there is no literalData, we don't have any metadata
metadata = null;
} }
if (encryptedData.isIntegrityProtected()) { int endProgress;
updateProgress(R.string.progress_verifying_integrity, 95, 100); if (signature != null) {
endProgress = 90;
if (encryptedData.verify()) { } else if (esResult != null && esResult.encryptedData.isIntegrityProtected()) {
log.add(LogType.MSG_DC_INTEGRITY_CHECK_OK, indent); endProgress = 95;
} else {
log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
} else { } else {
// If no valid signature is present: endProgress = 100;
// Handle missing integrity protection like failed integrity protection! }
// The MDC packet can be stripped by an attacker! ProgressScaler progressScaler =
Log.d(Constants.TAG, "MDC fail"); new ProgressScaler(mProgressable, currentProgress, endProgress, 100);
if (!signatureResultBuilder.isValidSignature()) {
InputStream dataIn = literalData.getInputStream();
long alreadyWritten = 0;
long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition();
int length;
byte[] buffer = new byte[1 << 16];
while ((length = dataIn.read(buffer)) > 0) {
// Log.d(Constants.TAG, "read bytes: " + length);
if (out != null) {
out.write(buffer, 0, length);
}
// update signature buffer if signature is also present
if (signature != null) {
signature.update(buffer, 0, length);
}
alreadyWritten += length;
if (wholeSize > 0) {
long progress = 100 * alreadyWritten / wholeSize;
// stop at 100% for wrong file sizes...
if (progress > 100) {
progress = 100;
}
progressScaler.setProgress((int) progress, 100);
}
// TODO: slow annealing to fake a progress?
}
metadata = new OpenPgpMetadata(
originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten);
if (signature != null) {
updateProgress(R.string.progress_verifying_signature, 90, 100);
log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent);
PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject();
PGPSignature messageSignature = signatureList.get(signatureIndex);
// Verify signature
boolean validSignature = signature.verify(messageSignature);
if (validSignature) {
log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1);
} else {
log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1);
}
// check for insecure hash algorithms
if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) {
log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1);
signatureResultBuilder.setInsecure(true);
}
signatureResultBuilder.setValidSignature(validSignature);
}
indent -= 1;
if (esResult != null) {
if (esResult.encryptedData.isIntegrityProtected()) {
updateProgress(R.string.progress_verifying_integrity, 95, 100);
if (esResult.encryptedData.verify()) {
log.add(LogType.MSG_DC_INTEGRITY_CHECK_OK, indent);
} else {
log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent);
return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
}
} else if (signature == null) {
// If no signature is present, we *require* an MDC!
// Handle missing integrity protection like failed integrity protection!
// The MDC packet can be stripped by an attacker!
log.add(LogType.MSG_DC_INSECURE_MDC_MISSING, indent); log.add(LogType.MSG_DC_INSECURE_MDC_MISSING, indent);
decryptionResultBuilder.setInsecure(true); decryptionResultBuilder.setInsecure(true);
} }
@@ -874,11 +660,280 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
// Return a positive result, with metadata and verification info // Return a positive result, with metadata and verification info
DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
result.setCachedCryptoInputParcel(cryptoInput); result.setCachedCryptoInputParcel(cryptoInput);
result.setSignatureResult(signatureResultBuilder.build()); result.setSignatureResult(signatureResultBuilder.build());
result.setCharset(charset); result.setCharset(charset);
result.setDecryptionResult(decryptionResultBuilder.build()); result.setDecryptionResult(decryptionResultBuilder.build());
result.setDecryptionMetadata(metadata); result.setDecryptionMetadata(metadata);
return result;
}
private EncryptStreamResult handleEncryptedPacket(PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput,
PGPEncryptedDataList enc, OperationLog log, int indent, int currentProgress) throws PGPException {
// TODO is this necessary?
/*
else if (obj instanceof PGPEncryptedDataList) {
enc = (PGPEncryptedDataList) pgpF.nextObject();
}
*/
EncryptStreamResult result = new EncryptStreamResult();
boolean asymmetricPacketFound = false;
boolean symmetricPacketFound = false;
boolean anyPacketFound = false;
PGPPublicKeyEncryptedData encryptedDataAsymmetric = null;
PGPPBEEncryptedData encryptedDataSymmetric = null;
CanonicalizedSecretKey secretEncryptionKey = null;
Passphrase passphrase = null;
Iterator<?> it = enc.getEncryptedDataObjects();
// go through all objects and find one we can decrypt
while (it.hasNext()) {
Object obj = it.next();
if (obj instanceof PGPPublicKeyEncryptedData) {
anyPacketFound = true;
currentProgress += 2;
updateProgress(R.string.progress_finding_key, currentProgress, 100);
PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj;
long subKeyId = encData.getKeyID();
log.add(LogType.MSG_DC_ASYM, indent,
KeyFormattingUtils.convertKeyIdToHex(subKeyId));
CanonicalizedSecretKeyRing secretKeyRing;
try {
// get actual keyring object based on master key id
secretKeyRing = mProviderHelper.getCanonicalizedSecretKeyRing(
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId)
);
} catch (ProviderHelper.NotFoundException e) {
// continue with the next packet in the while loop
log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1);
continue;
}
if (secretKeyRing == null) {
// continue with the next packet in the while loop
log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1);
continue;
}
// allow only specific keys for decryption?
if (input.getAllowedKeyIds() != null) {
long masterKeyId = secretKeyRing.getMasterKeyId();
Log.d(Constants.TAG, "encData.getKeyID(): " + subKeyId);
Log.d(Constants.TAG, "mAllowedKeyIds: " + input.getAllowedKeyIds());
Log.d(Constants.TAG, "masterKeyId: " + masterKeyId);
if (!input.getAllowedKeyIds().contains(masterKeyId)) {
// this key is in our db, but NOT allowed!
// continue with the next packet in the while loop
result.skippedDisallowedKey = true;
log.add(LogType.MSG_DC_ASKIP_NOT_ALLOWED, indent + 1);
continue;
}
}
// get subkey which has been used for this encryption packet
secretEncryptionKey = secretKeyRing.getSecretKey(subKeyId);
if (secretEncryptionKey == null) {
// should actually never happen, so no need to be more specific.
log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1);
continue;
}
/* secret key exists in database and is allowed! */
asymmetricPacketFound = true;
encryptedDataAsymmetric = encData;
if (secretEncryptionKey.getSecretKeyType() == SecretKeyType.DIVERT_TO_CARD) {
passphrase = null;
} else if (cryptoInput.hasPassphrase()) {
passphrase = cryptoInput.getPassphrase();
} else {
// if no passphrase was explicitly set try to get it from the cache service
try {
// returns "" if key has no passphrase
passphrase = getCachedPassphrase(subKeyId);
log.add(LogType.MSG_DC_PASS_CACHED, indent + 1);
} catch (PassphraseCacheInterface.NoSecretKeyException e) {
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
// if passphrase was not cached, return here indicating that a passphrase is missing!
if (passphrase == null) {
log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1);
return result.with(new DecryptVerifyResult(log,
RequiredInputParcel.createRequiredDecryptPassphrase(
secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId()),
cryptoInput));
}
}
// check for insecure encryption key
if ( ! PgpSecurityConstants.isSecureKey(secretEncryptionKey)) {
log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1);
result.insecureEncryptionKey = true;
}
// break out of while, only decrypt the first packet where we have a key
break;
} else if (obj instanceof PGPPBEEncryptedData) {
anyPacketFound = true;
log.add(LogType.MSG_DC_SYM, indent);
if (!input.isAllowSymmetricDecryption()) {
log.add(LogType.MSG_DC_SYM_SKIP, indent + 1);
continue;
}
/*
* When mAllowSymmetricDecryption == true and we find a data packet here,
* we do not search for other available asymmetric packets!
*/
symmetricPacketFound = true;
encryptedDataSymmetric = (PGPPBEEncryptedData) obj;
// if no passphrase is given, return here
// indicating that a passphrase is missing!
if (!cryptoInput.hasPassphrase()) {
try {
passphrase = getCachedPassphrase(key.symmetric);
log.add(LogType.MSG_DC_PASS_CACHED, indent + 1);
} catch (PassphraseCacheInterface.NoSecretKeyException e) {
// nvm
}
if (passphrase == null) {
log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1);
return result.with(new DecryptVerifyResult(log,
RequiredInputParcel.createRequiredSymmetricPassphrase(),
cryptoInput));
}
} else {
passphrase = cryptoInput.getPassphrase();
}
// break out of while, only decrypt the first packet
break;
}
}
// More data, just acknowledge and ignore.
while (it.hasNext()) {
Object obj = it.next();
if (obj instanceof PGPPublicKeyEncryptedData) {
PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj;
long subKeyId = encData.getKeyID();
log.add(LogType.MSG_DC_TRAIL_ASYM, indent,
KeyFormattingUtils.convertKeyIdToHex(subKeyId));
} else if (obj instanceof PGPPBEEncryptedData) {
log.add(LogType.MSG_DC_TRAIL_SYM, indent);
} else {
log.add(LogType.MSG_DC_TRAIL_UNKNOWN, indent);
}
}
// we made sure above one of these two would be true
if (symmetricPacketFound) {
currentProgress += 2;
updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
PGPDigestCalculatorProvider digestCalcProvider = new JcaPGPDigestCalculatorProviderBuilder()
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build();
PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(
digestCalcProvider).setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
passphrase.getCharArray());
try {
result.cleartextStream = encryptedDataSymmetric.getDataStream(decryptorFactory);
} catch (PGPDataValidationException e) {
log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent + 1);
return result.with(new DecryptVerifyResult(log,
RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput));
}
result.encryptedData = encryptedDataSymmetric;
result.symmetricEncryptionAlgo = encryptedDataSymmetric.getSymmetricAlgorithm(decryptorFactory);
} else if (asymmetricPacketFound) {
currentProgress += 2;
updateProgress(R.string.progress_extracting_key, currentProgress, 100);
try {
log.add(LogType.MSG_DC_UNLOCKING, indent + 1);
if (!secretEncryptionKey.unlock(passphrase)) {
log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1);
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
} catch (PgpGeneralException e) {
log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1);
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
currentProgress += 2;
updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
CachingDataDecryptorFactory decryptorFactory
= secretEncryptionKey.getCachingDecryptorFactory(cryptoInput);
// special case: if the decryptor does not have a session key cached for this encrypted
// data, and can't actually decrypt on its own, return a pending intent
if (!decryptorFactory.canDecrypt()
&& !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
log.add(LogType.MSG_DC_PENDING_NFC, indent + 1);
return result.with(new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation(
secretEncryptionKey.getRing().getMasterKeyId(),
secretEncryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0]
), cryptoInput));
}
try {
result.cleartextStream = encryptedDataAsymmetric.getDataStream(decryptorFactory);
} catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) {
log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1);
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
result.symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
result.encryptedData = encryptedDataAsymmetric;
cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys());
} else {
// there wasn't even any useful data
if (!anyPacketFound) {
log.add(LogType.MSG_DC_ERROR_NO_DATA, indent + 1);
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_NO_DATA, log));
}
// there was data but key wasn't allowed
if (result.skippedDisallowedKey) {
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_KEY_DISALLOWED, log));
}
// no packet has been found where we have the corresponding secret key in our db
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
return result; return result;
} }

View File

@@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.pgp;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
@@ -116,32 +117,27 @@ public class PgpHelper {
} }
} }
public static String getPgpContent(CharSequence input) { public static String getPgpContent(@NonNull CharSequence input) {
// only decrypt if clipboard content is available and a pgp message or cleartext signature Log.dEscaped(Constants.TAG, "input: " + input);
if (!TextUtils.isEmpty(input)) {
Log.dEscaped(Constants.TAG, "input: " + input);
Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input); Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input);
if (matcher.matches()) {
String text = matcher.group(1);
text = fixPgpMessage(text);
Log.dEscaped(Constants.TAG, "input fixed: " + text);
return text;
} else {
matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input);
if (matcher.matches()) { if (matcher.matches()) {
String text = matcher.group(1); String text = matcher.group(1);
text = fixPgpMessage(text); text = fixPgpCleartextSignature(text);
Log.dEscaped(Constants.TAG, "input fixed: " + text); Log.dEscaped(Constants.TAG, "input fixed: " + text);
return text; return text;
} else { } else {
matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input); return null;
if (matcher.matches()) {
String text = matcher.group(1);
text = fixPgpCleartextSignature(text);
Log.dEscaped(Constants.TAG, "input fixed: " + text);
return text;
} else {
return null;
}
} }
} else {
return null;
} }
} }

View File

@@ -23,6 +23,7 @@ import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.bcpg.PublicKeyAlgorithmTags; import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags; import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
/** /**
@@ -42,24 +43,23 @@ public class PgpSecurityConstants {
* Whitelist of accepted symmetric encryption algorithms * Whitelist of accepted symmetric encryption algorithms
* all other algorithms are rejected with OpenPgpDecryptionResult.RESULT_INSECURE * all other algorithms are rejected with OpenPgpDecryptionResult.RESULT_INSECURE
*/ */
private static HashSet<Integer> sSymmetricAlgorithmsWhitelist = new HashSet<>(); private static HashSet<Integer> sSymmetricAlgorithmsWhitelist = new HashSet<>(Arrays.asList(
static { // General remarks: We try to keep the whitelist short to reduce attack surface
// General remarks: We try to keep the whitelist short to reduce attack surface // TODO: block IDEA?: Bad key schedule (weak keys), implementation difficulties (easy to make errors)
// TODO: block IDEA?: Bad key schedule (weak keys), implementation difficulties (easy to make errors) SymmetricKeyAlgorithmTags.IDEA,
sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.IDEA); SymmetricKeyAlgorithmTags.TRIPLE_DES, // a MUST in RFC
sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.TRIPLE_DES); // a MUST in RFC SymmetricKeyAlgorithmTags.CAST5, // default in many gpg, pgp versions, 128 bit key
sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.CAST5); // default in many gpg, pgp versions, 128 bit key // BLOWFISH: Twofish is the successor
// BLOWFISH: Twofish is the successor // SAFER: not used widely
// SAFER: not used widely // DES: < 128 bit security
// DES: < 128 bit security SymmetricKeyAlgorithmTags.AES_128,
sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_128); SymmetricKeyAlgorithmTags.AES_192,
sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_192); SymmetricKeyAlgorithmTags.AES_256,
sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_256); SymmetricKeyAlgorithmTags.TWOFISH // 128 bit
sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.TWOFISH); // 128 bit // CAMELLIA_128: not used widely
// CAMELLIA_128: not used widely // CAMELLIA_192: not used widely
// CAMELLIA_192: not used widely // CAMELLIA_256: not used widely
// CAMELLIA_256: not used widely ));
}
public static boolean isSecureSymmetricAlgorithm(int id) { public static boolean isSecureSymmetricAlgorithm(int id) {
return sSymmetricAlgorithmsWhitelist.contains(id); return sSymmetricAlgorithmsWhitelist.contains(id);
@@ -77,20 +77,19 @@ public class PgpSecurityConstants {
* ((collision resistance of 112-bits)) * ((collision resistance of 112-bits))
* Implementations SHOULD NOT sign SHA-256 hashes. They MUST NOT default to signing SHA-256 hashes. * Implementations SHOULD NOT sign SHA-256 hashes. They MUST NOT default to signing SHA-256 hashes.
*/ */
private static HashSet<Integer> sHashAlgorithmsWhitelist = new HashSet<>(); private static HashSet<Integer> sHashAlgorithmsWhitelist = new HashSet<>(Arrays.asList(
static { // MD5: broken
// MD5: broken // SHA1: broken
// SHA1: broken // RIPEMD160: same security properties as SHA1
// RIPEMD160: same security properties as SHA1 // DOUBLE_SHA: not used widely
// DOUBLE_SHA: not used widely // MD2: not used widely
// MD2: not used widely // TIGER_192: not used widely
// TIGER_192: not used widely // HAVAL_5_160: not used widely
// HAVAL_5_160: not used widely HashAlgorithmTags.SHA256, // compatibility for old Mailvelope versions
sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA256); // compatibility for old Mailvelope versions HashAlgorithmTags.SHA384,
sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA384); HashAlgorithmTags.SHA512
sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA512); // SHA224: Not used widely, Yahoo argues against it
// SHA224: Not used widely, Yahoo argues against it ));
}
public static boolean isSecureHashAlgorithm(int id) { public static boolean isSecureHashAlgorithm(int id) {
return sHashAlgorithmsWhitelist.contains(id); return sHashAlgorithmsWhitelist.contains(id);
@@ -106,12 +105,11 @@ public class PgpSecurityConstants {
* bitlength less than 1023 bits. * bitlength less than 1023 bits.
* Implementations MUST NOT accept any RSA keys with bitlength less than 2047 bits after January 1, 2016. * Implementations MUST NOT accept any RSA keys with bitlength less than 2047 bits after January 1, 2016.
*/ */
private static HashSet<String> sCurveWhitelist = new HashSet<>(); private static HashSet<String> sCurveWhitelist = new HashSet<>(Arrays.asList(
static { NISTNamedCurves.getOID("P-256").getId(),
sCurveWhitelist.add(NISTNamedCurves.getOID("P-256").getId()); NISTNamedCurves.getOID("P-384").getId(),
sCurveWhitelist.add(NISTNamedCurves.getOID("P-384").getId()); NISTNamedCurves.getOID("P-521").getId()
sCurveWhitelist.add(NISTNamedCurves.getOID("P-521").getId()); ));
}
public static boolean isSecureKey(CanonicalizedPublicKey key) { public static boolean isSecureKey(CanonicalizedPublicKey key) {
switch (key.getAlgorithm()) { switch (key.getAlgorithm()) {

View File

@@ -216,17 +216,6 @@ public class UncachedKeyRing implements Serializable {
} }
public boolean containsSubkey(String expectedFingerprint) {
Iterator<PGPPublicKey> it = mRing.getPublicKeys();
while (it.hasNext()) {
if (KeyFormattingUtils.convertFingerprintToHex(
it.next().getFingerprint()).equalsIgnoreCase(expectedFingerprint)) {
return true;
}
}
return false;
}
public interface IteratorWithIOThrow<E> { public interface IteratorWithIOThrow<E> {
public boolean hasNext() throws IOException; public boolean hasNext() throws IOException;
public E next() throws IOException; public E next() throws IOException;

View File

@@ -368,4 +368,5 @@ public class UncachedPublicKey {
return calendar.getTime(); return calendar.getTime();
} }
} }

View File

@@ -1,4 +1,8 @@
/* /*
<<<<<<< HEAD
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
=======
>>>>>>> development
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@@ -37,6 +41,7 @@ public class WrappedUserAttribute implements Serializable {
public static final int UAT_NONE = 0; public static final int UAT_NONE = 0;
public static final int UAT_IMAGE = UserAttributeSubpacketTags.IMAGE_ATTRIBUTE; public static final int UAT_IMAGE = UserAttributeSubpacketTags.IMAGE_ATTRIBUTE;
public static final int UAT_URI_ATTRIBUTE = 101;
private PGPUserAttributeSubpacketVector mVector; private PGPUserAttributeSubpacketVector mVector;
@@ -77,7 +82,7 @@ public class WrappedUserAttribute implements Serializable {
public static WrappedUserAttribute fromData (byte[] data) throws IOException { public static WrappedUserAttribute fromData (byte[] data) throws IOException {
UserAttributeSubpacketInputStream in = UserAttributeSubpacketInputStream in =
new UserAttributeSubpacketInputStream(new ByteArrayInputStream(data)); new UserAttributeSubpacketInputStream(new ByteArrayInputStream(data));
ArrayList<UserAttributeSubpacket> list = new ArrayList<UserAttributeSubpacket>(); ArrayList<UserAttributeSubpacket> list = new ArrayList<>();
while (in.available() > 0) { while (in.available() > 0) {
list.add(in.readPacket()); list.add(in.readPacket());
} }
@@ -121,6 +126,7 @@ public class WrappedUserAttribute implements Serializable {
private void readObjectNoData() throws ObjectStreamException { private void readObjectNoData() throws ObjectStreamException {
} }
@SuppressWarnings("SimplifiableIfStatement")
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (!WrappedUserAttribute.class.isInstance(o)) { if (!WrappedUserAttribute.class.isInstance(o)) {

View File

@@ -51,6 +51,11 @@ public class KeychainContract {
String EXPIRY = "expiry"; String EXPIRY = "expiry";
} }
interface UpdatedKeysColumns {
String MASTER_KEY_ID = "master_key_id"; // not a database id
String LAST_UPDATED = "last_updated"; // time since epoch in seconds
}
interface UserPacketsColumns { interface UserPacketsColumns {
String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID
String TYPE = "type"; // not a database id String TYPE = "type"; // not a database id
@@ -97,6 +102,8 @@ public class KeychainContract {
public static final String BASE_KEY_RINGS = "key_rings"; public static final String BASE_KEY_RINGS = "key_rings";
public static final String BASE_UPDATED_KEYS = "updated_keys";
public static final String PATH_UNIFIED = "unified"; public static final String PATH_UNIFIED = "unified";
public static final String PATH_FIND = "find"; public static final String PATH_FIND = "find";
@@ -106,6 +113,7 @@ public class KeychainContract {
public static final String PATH_PUBLIC = "public"; public static final String PATH_PUBLIC = "public";
public static final String PATH_SECRET = "secret"; public static final String PATH_SECRET = "secret";
public static final String PATH_USER_IDS = "user_ids"; public static final String PATH_USER_IDS = "user_ids";
public static final String PATH_LINKED_IDS = "linked_ids";
public static final String PATH_KEYS = "keys"; public static final String PATH_KEYS = "keys";
public static final String PATH_CERTS = "certs"; public static final String PATH_CERTS = "certs";
@@ -234,6 +242,16 @@ public class KeychainContract {
} }
public static class UpdatedKeys implements UpdatedKeysColumns, BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
.appendPath(BASE_UPDATED_KEYS).build();
public static final String CONTENT_TYPE
= "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.updated_keys";
public static final String CONTENT_ITEM_TYPE
= "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.updated_keys";
}
public static class UserPackets implements UserPacketsColumns, BaseColumns { public static class UserPackets implements UserPacketsColumns, BaseColumns {
public static final String VERIFIED = "verified"; public static final String VERIFIED = "verified";
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
@@ -262,6 +280,11 @@ public class KeychainContract {
public static Uri buildUserIdsUri(Uri uri) { public static Uri buildUserIdsUri(Uri uri) {
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build(); return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build();
} }
public static Uri buildLinkedIdsUri(Uri uri) {
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_LINKED_IDS).build();
}
} }
public static class ApiApps implements ApiAppsColumns, BaseColumns { public static class ApiApps implements ApiAppsColumns, BaseColumns {
@@ -350,7 +373,14 @@ public class KeychainContract {
} }
public static Uri buildCertsUri(Uri uri) { public static Uri buildCertsUri(Uri uri) {
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_CERTS).build(); return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
.appendPath(PATH_CERTS).build();
}
public static Uri buildLinkedIdCertsUri(Uri uri, int rank) {
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
.appendPath(PATH_LINKED_IDS).appendPath(Integer.toString(rank))
.appendPath(PATH_CERTS).build();
} }
} }

View File

@@ -34,6 +34,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeysColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns;
import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity; import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
@@ -53,7 +54,7 @@ import java.io.IOException;
*/ */
public class KeychainDatabase extends SQLiteOpenHelper { public class KeychainDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "openkeychain.db"; private static final String DATABASE_NAME = "openkeychain.db";
private static final int DATABASE_VERSION = 11; private static final int DATABASE_VERSION = 12;
static Boolean apgHack = false; static Boolean apgHack = false;
private Context mContext; private Context mContext;
@@ -61,6 +62,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
String KEY_RINGS_PUBLIC = "keyrings_public"; String KEY_RINGS_PUBLIC = "keyrings_public";
String KEY_RINGS_SECRET = "keyrings_secret"; String KEY_RINGS_SECRET = "keyrings_secret";
String KEYS = "keys"; String KEYS = "keys";
String UPDATED_KEYS = "updated_keys";
String USER_PACKETS = "user_packets"; String USER_PACKETS = "user_packets";
String CERTS = "certs"; String CERTS = "certs";
String API_APPS = "api_apps"; String API_APPS = "api_apps";
@@ -144,6 +146,14 @@ public class KeychainDatabase extends SQLiteOpenHelper {
+ Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE" + Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE"
+ ")"; + ")";
private static final String CREATE_UPDATE_KEYS =
"CREATE TABLE IF NOT EXISTS " + Tables.UPDATED_KEYS + " ("
+ UpdatedKeysColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY, "
+ UpdatedKeysColumns.LAST_UPDATED + " INTEGER, "
+ "FOREIGN KEY(" + UpdatedKeysColumns.MASTER_KEY_ID + ") REFERENCES "
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
+ ")";
private static final String CREATE_API_APPS = private static final String CREATE_API_APPS =
"CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + " (" "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + " ("
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
@@ -206,6 +216,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
db.execSQL(CREATE_KEYS); db.execSQL(CREATE_KEYS);
db.execSQL(CREATE_USER_PACKETS); db.execSQL(CREATE_USER_PACKETS);
db.execSQL(CREATE_CERTS); db.execSQL(CREATE_CERTS);
db.execSQL(CREATE_UPDATE_KEYS);
db.execSQL(CREATE_API_APPS); db.execSQL(CREATE_API_APPS);
db.execSQL(CREATE_API_APPS_ACCOUNTS); db.execSQL(CREATE_API_APPS_ACCOUNTS);
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
@@ -278,8 +289,11 @@ public class KeychainDatabase extends SQLiteOpenHelper {
// fix problems in database, see #1402 for details // fix problems in database, see #1402 for details
// https://github.com/open-keychain/open-keychain/issues/1402 // https://github.com/open-keychain/open-keychain/issues/1402
db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3"); db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3");
case 12:
db.execSQL(CREATE_UPDATE_KEYS);
if (oldVersion == 10) { if (oldVersion == 10) {
// no consolidate if we are updating from 10, we're just here for the api_accounts fix // no consolidate if we are updating from 10, we're just here for
// the api_accounts fix and the new update keys table
return; return;
} }

View File

@@ -31,6 +31,7 @@ import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
@@ -38,6 +39,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
@@ -62,6 +64,8 @@ public class KeychainProvider extends ContentProvider {
private static final int KEY_RING_SECRET = 204; private static final int KEY_RING_SECRET = 204;
private static final int KEY_RING_CERTS = 205; private static final int KEY_RING_CERTS = 205;
private static final int KEY_RING_CERTS_SPECIFIC = 206; private static final int KEY_RING_CERTS_SPECIFIC = 206;
private static final int KEY_RING_LINKED_IDS = 207;
private static final int KEY_RING_LINKED_ID_CERTS = 208;
private static final int API_APPS = 301; private static final int API_APPS = 301;
private static final int API_APPS_BY_PACKAGE_NAME = 302; private static final int API_APPS_BY_PACKAGE_NAME = 302;
@@ -72,6 +76,9 @@ public class KeychainProvider extends ContentProvider {
private static final int KEY_RINGS_FIND_BY_EMAIL = 400; private static final int KEY_RINGS_FIND_BY_EMAIL = 400;
private static final int KEY_RINGS_FIND_BY_SUBKEY = 401; private static final int KEY_RINGS_FIND_BY_SUBKEY = 401;
private static final int UPDATED_KEYS = 500;
private static final int UPDATED_KEYS_SPECIFIC = 501;
protected UriMatcher mUriMatcher; protected UriMatcher mUriMatcher;
/** /**
@@ -127,6 +134,9 @@ public class KeychainProvider extends ContentProvider {
* key_rings/_/unified * key_rings/_/unified
* key_rings/_/keys * key_rings/_/keys
* key_rings/_/user_ids * key_rings/_/user_ids
* key_rings/_/linked_ids
* key_rings/_/linked_ids/_
* key_rings/_/linked_ids/_/certs
* key_rings/_/public * key_rings/_/public
* key_rings/_/secret * key_rings/_/secret
* key_rings/_/certs * key_rings/_/certs
@@ -142,6 +152,13 @@ public class KeychainProvider extends ContentProvider {
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ KeychainContract.PATH_USER_IDS, + KeychainContract.PATH_USER_IDS,
KEY_RING_USER_IDS); KEY_RING_USER_IDS);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ KeychainContract.PATH_LINKED_IDS,
KEY_RING_LINKED_IDS);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ KeychainContract.PATH_LINKED_IDS + "/*/"
+ KeychainContract.PATH_CERTS,
KEY_RING_LINKED_ID_CERTS);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ KeychainContract.PATH_PUBLIC, + KeychainContract.PATH_PUBLIC,
KEY_RING_PUBLIC); KEY_RING_PUBLIC);
@@ -179,6 +196,12 @@ public class KeychainProvider extends ContentProvider {
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/" matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/"
+ KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS); + KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS);
/**
* to access table containing last updated dates of keys
*/
matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS, UPDATED_KEYS);
matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS + "/*", UPDATED_KEYS_SPECIFIC);
return matcher; return matcher;
} }
@@ -218,6 +241,11 @@ public class KeychainProvider extends ContentProvider {
case KEY_RING_SECRET: case KEY_RING_SECRET:
return KeyRings.CONTENT_ITEM_TYPE; return KeyRings.CONTENT_ITEM_TYPE;
case UPDATED_KEYS:
return UpdatedKeys.CONTENT_TYPE;
case UPDATED_KEYS_SPECIFIC:
return UpdatedKeys.CONTENT_ITEM_TYPE;
case API_APPS: case API_APPS:
return ApiApps.CONTENT_TYPE; return ApiApps.CONTENT_TYPE;
@@ -477,7 +505,8 @@ public class KeychainProvider extends ContentProvider {
} }
case KEY_RINGS_USER_IDS: case KEY_RINGS_USER_IDS:
case KEY_RING_USER_IDS: { case KEY_RING_USER_IDS:
case KEY_RING_LINKED_IDS: {
HashMap<String, String> projectionMap = new HashMap<>(); HashMap<String, String> projectionMap = new HashMap<>();
projectionMap.put(UserPackets._ID, Tables.USER_PACKETS + ".oid AS _id"); projectionMap.put(UserPackets._ID, Tables.USER_PACKETS + ".oid AS _id");
projectionMap.put(UserPackets.MASTER_KEY_ID, Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID); projectionMap.put(UserPackets.MASTER_KEY_ID, Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID);
@@ -502,13 +531,15 @@ public class KeychainProvider extends ContentProvider {
groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID
+ ", " + Tables.USER_PACKETS + "." + UserPackets.RANK; + ", " + Tables.USER_PACKETS + "." + UserPackets.RANK;
// for now, we only respect user ids here, so TYPE must be NULL if (match == KEY_RING_LINKED_IDS) {
// TODO expand with KEY_RING_USER_PACKETS query type which lifts this restriction qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " = "
qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"); + WrappedUserAttribute.UAT_URI_ATTRIBUTE);
} else {
qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
}
// If we are searching for a particular keyring's ids, add where // If we are searching for a particular keyring's ids, add where
if (match == KEY_RING_USER_IDS) { if (match == KEY_RING_USER_IDS || match == KEY_RING_LINKED_IDS) {
// TODO remove with the thing above
qb.appendWhere(" AND "); qb.appendWhere(" AND ");
qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = "); qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(1)); qb.appendWhereEscapeString(uri.getPathSegments().get(1));
@@ -559,7 +590,8 @@ public class KeychainProvider extends ContentProvider {
} }
case KEY_RING_CERTS: case KEY_RING_CERTS:
case KEY_RING_CERTS_SPECIFIC: { case KEY_RING_CERTS_SPECIFIC:
case KEY_RING_LINKED_ID_CERTS: {
HashMap<String, String> projectionMap = new HashMap<>(); HashMap<String, String> projectionMap = new HashMap<>();
projectionMap.put(Certs._ID, Tables.CERTS + ".oid AS " + Certs._ID); projectionMap.put(Certs._ID, Tables.CERTS + ".oid AS " + Certs._ID);
projectionMap.put(Certs.MASTER_KEY_ID, Tables.CERTS + "." + Certs.MASTER_KEY_ID); projectionMap.put(Certs.MASTER_KEY_ID, Tables.CERTS + "." + Certs.MASTER_KEY_ID);
@@ -580,10 +612,6 @@ public class KeychainProvider extends ContentProvider {
+ " AND " + " AND "
+ Tables.CERTS + "." + Certs.RANK + " = " + Tables.CERTS + "." + Certs.RANK + " = "
+ Tables.USER_PACKETS + "." + UserPackets.RANK + Tables.USER_PACKETS + "." + UserPackets.RANK
// for now, we only return user ids here, so TYPE must be NULL
// TODO at some point, we should lift this restriction
+ " AND "
+ Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"
+ ") LEFT JOIN " + Tables.USER_PACKETS + " AS signer ON (" + ") LEFT JOIN " + Tables.USER_PACKETS + " AS signer ON ("
+ Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = " + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = "
+ "signer." + UserPackets.MASTER_KEY_ID + "signer." + UserPackets.MASTER_KEY_ID
@@ -603,6 +631,33 @@ public class KeychainProvider extends ContentProvider {
qb.appendWhereEscapeString(uri.getPathSegments().get(4)); qb.appendWhereEscapeString(uri.getPathSegments().get(4));
} }
if (match == KEY_RING_LINKED_ID_CERTS) {
qb.appendWhere(" AND " + Tables.USER_PACKETS + "."
+ UserPackets.TYPE + " IS NOT NULL");
qb.appendWhere(" AND " + Tables.USER_PACKETS + "."
+ UserPackets.RANK + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(3));
} else {
qb.appendWhere(" AND " + Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
}
break;
}
case UPDATED_KEYS:
case UPDATED_KEYS_SPECIFIC: {
HashMap<String, String> projectionMap = new HashMap<>();
qb.setTables(Tables.UPDATED_KEYS);
projectionMap.put(UpdatedKeys.MASTER_KEY_ID, Tables.UPDATED_KEYS + "."
+ UpdatedKeys.MASTER_KEY_ID);
projectionMap.put(UpdatedKeys.LAST_UPDATED, Tables.UPDATED_KEYS + "."
+ UpdatedKeys.LAST_UPDATED);
qb.setProjectionMap(projectionMap);
if (match == UPDATED_KEYS_SPECIFIC) {
qb.appendWhere(UpdatedKeys.MASTER_KEY_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
}
break; break;
} }
@@ -726,6 +781,12 @@ public class KeychainProvider extends ContentProvider {
keyId = values.getAsLong(Certs.MASTER_KEY_ID); keyId = values.getAsLong(Certs.MASTER_KEY_ID);
break; break;
} }
case UPDATED_KEYS: {
long updatedKeyId = db.replace(Tables.UPDATED_KEYS, null, values);
rowUri = UpdatedKeys.CONTENT_URI.buildUpon().appendPath("" + updatedKeyId)
.build();
break;
}
case API_APPS: { case API_APPS: {
db.insertOrThrow(Tables.API_APPS, null, values); db.insertOrThrow(Tables.API_APPS, null, values);
break; break;

View File

@@ -32,10 +32,15 @@ import android.support.v4.util.LongSparseArray;
import org.spongycastle.bcpg.CompressionAlgorithmTags; import org.spongycastle.bcpg.CompressionAlgorithmTags;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
@@ -50,7 +55,6 @@ import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
import org.sufficientlysecure.keychain.pgp.WrappedSignature; import org.sufficientlysecure.keychain.pgp.WrappedSignature;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
@@ -59,14 +63,12 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
import org.sufficientlysecure.keychain.remote.AccountSettings; import org.sufficientlysecure.keychain.remote.AccountSettings;
import org.sufficientlysecure.keychain.remote.AppSettings; import org.sufficientlysecure.keychain.remote.AppSettings;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableFileCache; import org.sufficientlysecure.keychain.util.ParcelableFileCache;
import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.ProgressFixedScaler; import org.sufficientlysecure.keychain.util.ProgressFixedScaler;
import org.sufficientlysecure.keychain.util.ProgressScaler; import org.sufficientlysecure.keychain.util.ProgressScaler;
import org.sufficientlysecure.keychain.util.Utf8Util; import org.sufficientlysecure.keychain.util.Utf8Util;
@@ -82,6 +84,7 @@ import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
/** /**
* This class contains high level methods for database access. Despite its * This class contains high level methods for database access. Despite its
@@ -685,6 +688,36 @@ public class ProviderHelper {
mIndent -= 1; mIndent -= 1;
} }
// before deleting key, retrieve it's last updated time
final int INDEX_MASTER_KEY_ID = 0;
final int INDEX_LAST_UPDATED = 1;
Cursor lastUpdatedCursor = mContentResolver.query(
UpdatedKeys.CONTENT_URI,
new String[]{
UpdatedKeys.MASTER_KEY_ID,
UpdatedKeys.LAST_UPDATED
},
UpdatedKeys.MASTER_KEY_ID + " = ?",
new String[]{"" + masterKeyId},
null
);
if (lastUpdatedCursor.moveToNext()) {
// there was an entry to re-insert
// this operation must happen after the new key is inserted
ContentValues lastUpdatedEntry = new ContentValues(2);
lastUpdatedEntry.put(UpdatedKeys.MASTER_KEY_ID,
lastUpdatedCursor.getLong(INDEX_MASTER_KEY_ID));
lastUpdatedEntry.put(UpdatedKeys.LAST_UPDATED,
lastUpdatedCursor.getLong(INDEX_LAST_UPDATED));
operations.add(
ContentProviderOperation
.newInsert(UpdatedKeys.CONTENT_URI)
.withValues(lastUpdatedEntry)
.build()
);
}
lastUpdatedCursor.close();
try { try {
// delete old version of this keyRing, which also deletes all keys and userIds on cascade // delete old version of this keyRing, which also deletes all keys and userIds on cascade
int deleted = mContentResolver.delete( int deleted = mContentResolver.delete(
@@ -738,6 +771,11 @@ public class ProviderHelper {
if (type != o.type) { if (type != o.type) {
return type == null ? -1 : 1; return type == null ? -1 : 1;
} }
// if one is *trusted* but the other isn't, that one comes first
// this overrides the primary attribute, even!
if ( (trustedCerts.size() == 0) != (o.trustedCerts.size() == 0) ) {
return trustedCerts.size() > o.trustedCerts.size() ? -1 : 1;
}
// if one key is primary but the other isn't, the primary one always comes first // if one key is primary but the other isn't, the primary one always comes first
if (isPrimary != o.isPrimary) { if (isPrimary != o.isPrimary) {
return isPrimary ? -1 : 1; return isPrimary ? -1 : 1;
@@ -845,7 +883,7 @@ public class ProviderHelper {
} }
public SaveKeyringResult savePublicKeyRing(UncachedKeyRing keyRing) { public SaveKeyringResult savePublicKeyRing(UncachedKeyRing keyRing) {
return savePublicKeyRing(keyRing, new ProgressScaler()); return savePublicKeyRing(keyRing, new ProgressScaler(), null);
} }
/** /**
@@ -854,7 +892,7 @@ public class ProviderHelper {
* This is a high level method, which takes care of merging all new information into the old and * This is a high level method, which takes care of merging all new information into the old and
* keep public and secret keyrings in sync. * keep public and secret keyrings in sync.
*/ */
public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress) { public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress, String expectedFingerprint) {
try { try {
long masterKeyId = publicRing.getMasterKeyId(); long masterKeyId = publicRing.getMasterKeyId();
@@ -927,6 +965,17 @@ public class ProviderHelper {
canSecretRing = null; canSecretRing = null;
} }
// If we have an expected fingerprint, make sure it matches
if (expectedFingerprint != null) {
if (!canPublicRing.containsSubkey(expectedFingerprint)) {
log(LogType.MSG_IP_FINGERPRINT_ERROR);
return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null);
} else {
log(LogType.MSG_IP_FINGERPRINT_OK);
}
}
int result = saveCanonicalizedPublicKeyRing(canPublicRing, progress, canSecretRing != null); int result = saveCanonicalizedPublicKeyRing(canPublicRing, progress, canSecretRing != null);
// Save the saved keyring (if any) // Save the saved keyring (if any)
@@ -1239,6 +1288,28 @@ public class ProviderHelper {
} }
// 2. wipe database (IT'S DANGEROUS) // 2. wipe database (IT'S DANGEROUS)
// first, backup our list of updated key times
ArrayList<ContentValues> updatedKeysValues = new ArrayList<>();
final int INDEX_MASTER_KEY_ID = 0;
final int INDEX_LAST_UPDATED = 1;
Cursor lastUpdatedCursor = mContentResolver.query(
UpdatedKeys.CONTENT_URI,
new String[]{
UpdatedKeys.MASTER_KEY_ID,
UpdatedKeys.LAST_UPDATED
},
null, null, null);
while (lastUpdatedCursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(UpdatedKeys.MASTER_KEY_ID,
lastUpdatedCursor.getLong(INDEX_MASTER_KEY_ID));
values.put(UpdatedKeys.LAST_UPDATED,
lastUpdatedCursor.getLong(INDEX_LAST_UPDATED));
updatedKeysValues.add(values);
}
lastUpdatedCursor.close();
log.add(LogType.MSG_CON_DB_CLEAR, indent); log.add(LogType.MSG_CON_DB_CLEAR, indent);
mContentResolver.delete(KeyRings.buildUnifiedKeyRingsUri(), null, null); mContentResolver.delete(KeyRings.buildUnifiedKeyRingsUri(), null, null);
@@ -1288,6 +1359,10 @@ public class ProviderHelper {
new ProgressFixedScaler(progress, 25, 99, 100, R.string.progress_con_reimport)) new ProgressFixedScaler(progress, 25, 99, 100, R.string.progress_con_reimport))
.serialKeyRingImport(itPublics, numPublics, null, null); .serialKeyRingImport(itPublics, numPublics, null, null);
log.add(result, indent); log.add(result, indent);
// re-insert our backed up list of updated key times
// TODO: can this cause issues in case a public key re-import failed?
mContentResolver.bulkInsert(UpdatedKeys.CONTENT_URI,
updatedKeysValues.toArray(new ContentValues[updatedKeysValues.size()]));
} else { } else {
log.add(LogType.MSG_CON_REIMPORT_PUBLIC_SKIP, indent); log.add(LogType.MSG_CON_REIMPORT_PUBLIC_SKIP, indent);
} }
@@ -1397,6 +1472,14 @@ public class ProviderHelper {
return getKeyRingAsArmoredString(data); return getKeyRingAsArmoredString(data);
} }
public Uri renewKeyLastUpdatedTime(long masterKeyId, long time, TimeUnit timeUnit) {
ContentValues values = new ContentValues();
values.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId);
values.put(UpdatedKeys.LAST_UPDATED, timeUnit.toSeconds(time));
return mContentResolver.insert(UpdatedKeys.CONTENT_URI, values);
}
public ArrayList<String> getRegisteredApiApps() { public ArrayList<String> getRegisteredApiApps() {
Cursor cursor = mContentResolver.query(ApiApps.CONTENT_URI, null, null, null, null); Cursor cursor = mContentResolver.query(ApiApps.CONTENT_URI, null, null, null, null);

View File

@@ -25,6 +25,7 @@ import android.net.Uri;
import android.os.IBinder; import android.os.IBinder;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import org.openintents.openpgp.IOpenPgpService; import org.openintents.openpgp.IOpenPgpService;
@@ -38,7 +39,7 @@ import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel;
import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
@@ -65,8 +66,10 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
public class OpenPgpService extends RemoteService { public class OpenPgpService extends RemoteService {
@@ -533,7 +536,7 @@ public class OpenPgpService extends RemoteService {
byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE); byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE);
PgpDecryptVerify op = new PgpDecryptVerify(this, mProviderHelper, null); PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(this, mProviderHelper, null);
long inputLength = inputStream.available(); long inputLength = inputStream.available();
InputData inputData = new InputData(inputStream, inputLength); InputData inputData = new InputData(inputStream, inputLength);
@@ -715,28 +718,40 @@ public class OpenPgpService extends RemoteService {
} }
private Intent getSignKeyIdImpl(Intent data) { private Intent getSignKeyIdImpl(Intent data) {
String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID); // if data already contains EXTRA_SIGN_KEY_ID, it has been executed again
// after user interaction. Then, we just need to return the long again!
if (data.hasExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
long signKeyId = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID,
Constants.key.none);
Intent intent = new Intent(getBaseContext(), SelectSignKeyIdActivity.class); Intent result = new Intent();
String currentPkg = getCurrentCallingPackage(); result.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, signKeyId);
intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(currentPkg)); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); return result;
intent.putExtra(SelectSignKeyIdActivity.EXTRA_DATA, data); } else {
String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID);
PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, Intent intent = new Intent(getBaseContext(), SelectSignKeyIdActivity.class);
intent, String currentPkg = getCurrentCallingPackage();
PendingIntent.FLAG_CANCEL_CURRENT); intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(currentPkg));
intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId);
intent.putExtra(SelectSignKeyIdActivity.EXTRA_DATA, data);
// return PendingIntent to be executed by client PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0,
Intent result = new Intent(); intent,
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); PendingIntent.FLAG_CANCEL_CURRENT);
result.putExtra(OpenPgpApi.RESULT_INTENT, pi);
return result; // return PendingIntent to be executed by client
Intent result = new Intent();
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
result.putExtra(OpenPgpApi.RESULT_INTENT, pi);
return result;
}
} }
private Intent getKeyIdsImpl(Intent data) { private Intent getKeyIdsImpl(Intent data) {
// if data already contains key ids extra GET_KEY_IDS has been executed again // if data already contains EXTRA_KEY_IDS, it has been executed again
// after user interaction. Then, we just need to return the array again! // after user interaction. Then, we just need to return the array again!
if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) { if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) {
long[] keyIdsArray = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); long[] keyIdsArray = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS);
@@ -800,19 +815,14 @@ public class OpenPgpService extends RemoteService {
} }
// version code is required and needs to correspond to version code of service! // version code is required and needs to correspond to version code of service!
// History of versions in org.openintents.openpgp.util.OpenPgpApi // History of versions in openpgp-api's CHANGELOG.md
// we support 3, 4, 5, 6 List<Integer> supportedVersions = Arrays.asList(3, 4, 5, 6, 7, 8, 9);
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 3 if (!supportedVersions.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) {
&& data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 4
&& data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 5
&& data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 6
&& data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 7
&& data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 8) {
Intent result = new Intent(); Intent result = new Intent();
OpenPgpError error = new OpenPgpError OpenPgpError error = new OpenPgpError
(OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n" (OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n"
+ "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\n" + "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\n"
+ "supported API versions: 3-8"); + "supported API versions: " + supportedVersions.toString());
result.putExtra(OpenPgpApi.RESULT_ERROR, error); result.putExtra(OpenPgpApi.RESULT_ERROR, error);
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
return result; return result;
@@ -830,67 +840,8 @@ public class OpenPgpService extends RemoteService {
private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() { private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() {
@Override @Override
public Intent execute(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) { public Intent execute(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) {
try { Log.w(Constants.TAG, "You are using a deprecated service which may lead to truncated data on return, please use IOpenPgpService2!");
Intent errorResult = checkRequirements(data); return executeInternal(data, input, output);
if (errorResult != null) {
return errorResult;
}
String action = data.getAction();
switch (action) {
case OpenPgpApi.ACTION_CLEARTEXT_SIGN: {
return signImpl(data, input, output, true);
}
case OpenPgpApi.ACTION_SIGN: {
// DEPRECATED: same as ACTION_CLEARTEXT_SIGN
Log.w(Constants.TAG, "You are using a deprecated API call, please use ACTION_CLEARTEXT_SIGN instead of ACTION_SIGN!");
return signImpl(data, input, output, true);
}
case OpenPgpApi.ACTION_DETACHED_SIGN: {
return signImpl(data, input, output, false);
}
case OpenPgpApi.ACTION_ENCRYPT: {
return encryptAndSignImpl(data, input, output, false);
}
case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: {
return encryptAndSignImpl(data, input, output, true);
}
case OpenPgpApi.ACTION_DECRYPT_VERIFY: {
return decryptAndVerifyImpl(data, input, output, false);
}
case OpenPgpApi.ACTION_DECRYPT_METADATA: {
return decryptAndVerifyImpl(data, input, output, true);
}
case OpenPgpApi.ACTION_GET_SIGN_KEY_ID: {
return getSignKeyIdImpl(data);
}
case OpenPgpApi.ACTION_GET_KEY_IDS: {
return getKeyIdsImpl(data);
}
case OpenPgpApi.ACTION_GET_KEY: {
return getKeyImpl(data);
}
default: {
return null;
}
}
} finally {
// always close input and output file descriptors even in error cases
if (input != null) {
try {
input.close();
} catch (IOException e) {
Log.e(Constants.TAG, "IOException when closing input ParcelFileDescriptor", e);
}
}
if (output != null) {
try {
output.close();
} catch (IOException e) {
Log.e(Constants.TAG, "IOException when closing output ParcelFileDescriptor", e);
}
}
}
} }
}; };
@@ -900,4 +851,68 @@ public class OpenPgpService extends RemoteService {
return mBinder; return mBinder;
} }
protected Intent executeInternal(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) {
try {
Intent errorResult = checkRequirements(data);
if (errorResult != null) {
return errorResult;
}
String action = data.getAction();
switch (action) {
case OpenPgpApi.ACTION_CLEARTEXT_SIGN: {
return signImpl(data, input, output, true);
}
case OpenPgpApi.ACTION_SIGN: {
// DEPRECATED: same as ACTION_CLEARTEXT_SIGN
Log.w(Constants.TAG, "You are using a deprecated API call, please use ACTION_CLEARTEXT_SIGN instead of ACTION_SIGN!");
return signImpl(data, input, output, true);
}
case OpenPgpApi.ACTION_DETACHED_SIGN: {
return signImpl(data, input, output, false);
}
case OpenPgpApi.ACTION_ENCRYPT: {
return encryptAndSignImpl(data, input, output, false);
}
case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: {
return encryptAndSignImpl(data, input, output, true);
}
case OpenPgpApi.ACTION_DECRYPT_VERIFY: {
return decryptAndVerifyImpl(data, input, output, false);
}
case OpenPgpApi.ACTION_DECRYPT_METADATA: {
return decryptAndVerifyImpl(data, input, output, true);
}
case OpenPgpApi.ACTION_GET_SIGN_KEY_ID: {
return getSignKeyIdImpl(data);
}
case OpenPgpApi.ACTION_GET_KEY_IDS: {
return getKeyIdsImpl(data);
}
case OpenPgpApi.ACTION_GET_KEY: {
return getKeyImpl(data);
}
default: {
return null;
}
}
} finally {
// always close input and output file descriptors even in error cases
if (input != null) {
try {
input.close();
} catch (IOException e) {
Log.e(Constants.TAG, "IOException when closing input ParcelFileDescriptor", e);
}
}
if (output != null) {
try {
output.close();
} catch (IOException e) {
Log.e(Constants.TAG, "IOException when closing output ParcelFileDescriptor", e);
}
}
}
}
} }

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.remote;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import org.openintents.openpgp.IOpenPgpService2;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class OpenPgpService2 extends OpenPgpService {
private Map<Long, ParcelFileDescriptor> mOutputPipeMap = new HashMap<Long, ParcelFileDescriptor>();
private long createKey(int id) {
int callingPid = Binder.getCallingPid();
return ((long) callingPid << 32) | ((long) id & 0xFFFFFFFL);
}
private final IOpenPgpService2.Stub mBinder = new IOpenPgpService2.Stub() {
@Override
public ParcelFileDescriptor createOutputPipe(int outputPipeId) {
try {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
mOutputPipeMap.put(createKey(outputPipeId), pipe[1]);
return pipe[0];
} catch (IOException e) {
Log.e(Constants.TAG, "IOException in OpenPgpService2", e);
return null;
}
}
@Override
public Intent execute(Intent data, ParcelFileDescriptor input, int outputPipeId) {
long key = createKey(outputPipeId);
ParcelFileDescriptor output = mOutputPipeMap.get(key);
mOutputPipeMap.remove(key);
return executeInternal(data, input, output);
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

View File

@@ -243,7 +243,7 @@ public class AppsListFragment extends ListFragment implements
null, null,
isInstalled(packageName), isInstalled(packageName),
1, // registered! 1, // registered!
R.drawable.ic_launcher // icon is retrieved later R.mipmap.ic_launcher // icon is retrieved later
}); });
break; break;
} }
@@ -265,7 +265,7 @@ public class AppsListFragment extends ListFragment implements
name, name,
isInstalled(packageName), isInstalled(packageName),
1, // registered! 1, // registered!
R.drawable.ic_launcher // icon is retrieved later R.mipmap.ic_launcher // icon is retrieved later
}); });
break; break;
} }

View File

@@ -40,14 +40,9 @@ public class SelectSignKeyIdActivity extends BaseActivity {
private static final int REQUEST_CODE_CREATE_KEY = 0x00008884; private static final int REQUEST_CODE_CREATE_KEY = 0x00008884;
private Uri mAppUri;
private String mPreferredUserId; private String mPreferredUserId;
private Intent mData; private Intent mData;
private SelectSignKeyIdListFragment mListFragment;
private TextView mActionCreateKey;
private TextView mNone;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -62,15 +57,15 @@ public class SelectSignKeyIdActivity extends BaseActivity {
} }
}); });
mActionCreateKey = (TextView) findViewById(R.id.api_select_sign_key_create_key); TextView createKeyButton = (TextView) findViewById(R.id.api_select_sign_key_create_key);
mActionCreateKey.setOnClickListener(new View.OnClickListener() { createKeyButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
createKey(mPreferredUserId); createKey(mPreferredUserId);
} }
}); });
mNone = (TextView) findViewById(R.id.api_select_sign_key_none); TextView noneButton = (TextView) findViewById(R.id.api_select_sign_key_none);
mNone.setOnClickListener(new View.OnClickListener() { noneButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
// 0 is "none" // 0 is "none"
@@ -82,16 +77,16 @@ public class SelectSignKeyIdActivity extends BaseActivity {
}); });
Intent intent = getIntent(); Intent intent = getIntent();
mAppUri = intent.getData(); Uri appUri = intent.getData();
mPreferredUserId = intent.getStringExtra(EXTRA_USER_ID); mPreferredUserId = intent.getStringExtra(EXTRA_USER_ID);
mData = intent.getParcelableExtra(EXTRA_DATA); mData = intent.getParcelableExtra(EXTRA_DATA);
if (mAppUri == null) { if (appUri == null) {
Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!");
finish(); finish();
return; return;
} else { } else {
Log.d(Constants.TAG, "uri: " + mAppUri); Log.d(Constants.TAG, "uri: " + appUri);
startListFragments(savedInstanceState, mAppUri, mData); startListFragments(savedInstanceState, appUri, mData);
} }
} }
@@ -113,11 +108,11 @@ public class SelectSignKeyIdActivity extends BaseActivity {
} }
// Create an instance of the fragments // Create an instance of the fragments
mListFragment = SelectSignKeyIdListFragment.newInstance(dataUri, data); SelectSignKeyIdListFragment listFragment = SelectSignKeyIdListFragment.newInstance(dataUri, data);
// Add the fragment to the 'fragment_container' FrameLayout // Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes! // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.replace(R.id.api_select_sign_key_list_fragment, mListFragment) .replace(R.id.api_select_sign_key_list_fragment, listFragment)
.commitAllowingStateLoss(); .commitAllowingStateLoss();
// do it immediately! // do it immediately!
getSupportFragmentManager().executePendingTransactions(); getSupportFragmentManager().executePendingTransactions();

View File

@@ -23,6 +23,9 @@ import android.os.Parcelable;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.Date;
import java.util.Map;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.ParcelableProxy;
@@ -86,17 +89,10 @@ public class CertifyActionsParcel implements Parcelable {
final public ArrayList<String> mUserIds; final public ArrayList<String> mUserIds;
final public ArrayList<WrappedUserAttribute> mUserAttributes; final public ArrayList<WrappedUserAttribute> mUserAttributes;
public CertifyAction(long masterKeyId, ArrayList<String> userIds) { public CertifyAction(long masterKeyId, List<String> userIds, List<WrappedUserAttribute> attributes) {
mMasterKeyId = masterKeyId; mMasterKeyId = masterKeyId;
mUserIds = userIds; mUserIds = userIds == null ? null : new ArrayList<>(userIds);
mUserAttributes = null; mUserAttributes = attributes == null ? null : new ArrayList<>(attributes);
}
public CertifyAction(long masterKeyId, ArrayList<String> userIds,
ArrayList<WrappedUserAttribute> attributes) {
mMasterKeyId = masterKeyId;
mUserIds = userIds;
mUserAttributes = attributes;
} }
} }

View File

@@ -45,7 +45,7 @@ public class ContactSyncAdapterService extends Service {
@Override @Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
final SyncResult syncResult) { final SyncResult syncResult) {
Log.d(Constants.TAG, "Performing a sync!"); Log.d(Constants.TAG, "Performing a contact sync!");
// TODO: Import is currently disabled for 2.8, until we implement proper origin management // TODO: Import is currently disabled for 2.8, until we implement proper origin management
// importDone.set(false); // importDone.set(false);
// KeychainApplication.setupAccountAsNeeded(ContactSyncAdapterService.this); // KeychainApplication.setupAccountAsNeeded(ContactSyncAdapterService.this);

View File

@@ -41,7 +41,7 @@ import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.RevokeOperation; import org.sufficientlysecure.keychain.operations.RevokeOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation; import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel;
@@ -112,7 +112,7 @@ public class KeychainService extends Service implements Progressable {
op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis), op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis),
outerThis, mActionCanceled); outerThis, mActionCanceled);
} else if (inputParcel instanceof PgpDecryptVerifyInputParcel) { } else if (inputParcel instanceof PgpDecryptVerifyInputParcel) {
op = new PgpDecryptVerify(outerThis, new ProviderHelper(outerThis), outerThis); op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof SaveKeyringParcel) { } else if (inputParcel instanceof SaveKeyringParcel) {
op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis,
mActionCanceled); mActionCanceled);

View File

@@ -0,0 +1,516 @@
package org.sufficientlysecure.keychain.service;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.PowerManager;
import android.os.SystemClock;
import android.support.v4.app.NotificationCompat;
import android.widget.Toast;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableProxy;
import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class KeyserverSyncAdapterService extends Service {
// how often a sync should be initiated, in s
public static final long SYNC_INTERVAL =
Constants.DEBUG_KEYSERVER_SYNC
? TimeUnit.MINUTES.toSeconds(2) : TimeUnit.DAYS.toSeconds(3);
// time since last update after which a key should be updated again, in s
public static final long KEY_UPDATE_LIMIT =
Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toSeconds(7);
// time by which a sync is postponed in case of a
public static final long SYNC_POSTPONE_TIME =
Constants.DEBUG_KEYSERVER_SYNC ? 30 * 1000 : TimeUnit.MINUTES.toMillis(5);
// Time taken by Orbot before a new circuit is created
public static final int ORBOT_CIRCUIT_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(10);
private static final String ACTION_IGNORE_TOR = "ignore_tor";
private static final String ACTION_UPDATE_ALL = "update_all";
private static final String ACTION_SYNC_NOW = "sync_now";
private static final String ACTION_DISMISS_NOTIFICATION = "cancel_sync";
private static final String ACTION_START_ORBOT = "start_orbot";
private static final String ACTION_CANCEL = "cancel";
private AtomicBoolean mCancelled = new AtomicBoolean(false);
@Override
public int onStartCommand(final Intent intent, int flags, final int startId) {
switch (intent.getAction()) {
case ACTION_CANCEL: {
mCancelled.set(true);
break;
}
// the reason for the separation betweyeen SYNC_NOW and UPDATE_ALL is so that starting
// the sync directly from the notification is possible while the screen is on with
// UPDATE_ALL, but a postponed sync is only started if screen is off
case ACTION_SYNC_NOW: {
// this checks for screen on/off before sync, and postpones the sync if on
ContentResolver.requestSync(
new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE),
Constants.PROVIDER_AUTHORITY,
new Bundle()
);
break;
}
case ACTION_UPDATE_ALL: {
// does not check for screen on/off
asyncKeyUpdate(this, new CryptoInputParcel());
break;
}
case ACTION_IGNORE_TOR: {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
asyncKeyUpdate(this, new CryptoInputParcel(ParcelableProxy.getForNoProxy()));
break;
}
case ACTION_START_ORBOT: {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
Intent startOrbot = new Intent(this, OrbotRequiredDialogActivity.class);
startOrbot.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_START_ORBOT, true);
Messenger messenger = new Messenger(
new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case OrbotRequiredDialogActivity.MESSAGE_ORBOT_STARTED: {
asyncKeyUpdate(KeyserverSyncAdapterService.this,
new CryptoInputParcel());
break;
}
case OrbotRequiredDialogActivity.MESSAGE_ORBOT_IGNORE: {
asyncKeyUpdate(KeyserverSyncAdapterService.this,
new CryptoInputParcel(
ParcelableProxy.getForNoProxy()));
break;
}
case OrbotRequiredDialogActivity.MESSAGE_DIALOG_CANCEL: {
// just stop service
stopSelf();
break;
}
}
}
}
);
startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_MESSENGER, messenger);
startActivity(startOrbot);
break;
}
case ACTION_DISMISS_NOTIFICATION: {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
stopSelf(startId);
break;
}
}
return START_NOT_STICKY;
}
private class KeyserverSyncAdapter extends AbstractThreadedSyncAdapter {
public KeyserverSyncAdapter() {
super(KeyserverSyncAdapterService.this, true);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
Log.d(Constants.TAG, "Performing a keyserver sync!");
PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this
.getSystemService(Context.POWER_SERVICE);
@SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20
boolean isScreenOn = pm.isScreenOn();
if (!isScreenOn) {
Intent serviceIntent = new Intent(KeyserverSyncAdapterService.this,
KeyserverSyncAdapterService.class);
serviceIntent.setAction(ACTION_UPDATE_ALL);
startService(serviceIntent);
} else {
postponeSync();
}
}
@Override
public void onSyncCanceled() {
super.onSyncCanceled();
cancelUpdates(KeyserverSyncAdapterService.this);
}
}
@Override
public IBinder onBind(Intent intent) {
return new KeyserverSyncAdapter().getSyncAdapterBinder();
}
private void handleUpdateResult(ImportKeyResult result) {
if (result.isPending()) {
// result is pending due to Orbot not being started
// try to start it silently, if disabled show notifications
new OrbotHelper.SilentStartManager() {
@Override
protected void onOrbotStarted() {
// retry the update
asyncKeyUpdate(KeyserverSyncAdapterService.this,
new CryptoInputParcel());
}
@Override
protected void onSilentStartDisabled() {
// show notification
NotificationManager manager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT,
getOrbotNoification(KeyserverSyncAdapterService.this));
}
}.startOrbotAndListen(this, false);
} else if (isUpdateCancelled()) {
Log.d(Constants.TAG, "Keyserver sync cancelled, postponing by" + SYNC_POSTPONE_TIME
+ "ms");
postponeSync();
} else {
Log.d(Constants.TAG, "Keyserver sync completed: Updated: " + result.mUpdatedKeys
+ " Failed: " + result.mBadKeys);
stopSelf();
}
}
private void postponeSync() {
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class);
serviceIntent.setAction(ACTION_SYNC_NOW);
PendingIntent pi = PendingIntent.getService(this, 0, serviceIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + SYNC_POSTPONE_TIME,
pi
);
}
private void asyncKeyUpdate(final Context context,
final CryptoInputParcel cryptoInputParcel) {
new Thread(new Runnable() {
@Override
public void run() {
ImportKeyResult result = updateKeysFromKeyserver(context, cryptoInputParcel);
handleUpdateResult(result);
}
}).start();
}
private synchronized ImportKeyResult updateKeysFromKeyserver(final Context context,
final CryptoInputParcel cryptoInputParcel) {
mCancelled.set(false);
ArrayList<ParcelableKeyRing> keyList = getKeysToUpdate(context);
if (isUpdateCancelled()) { // if we've already been cancelled
return new ImportKeyResult(OperationResult.RESULT_CANCELLED,
new OperationResult.OperationLog());
}
if (cryptoInputParcel.getParcelableProxy() == null) {
// no explicit proxy, retrieve from preferences. Check if we should do a staggered sync
if (Preferences.getPreferences(context).getProxyPrefs().torEnabled) {
return staggeredUpdate(context, keyList, cryptoInputParcel);
} else {
return directUpdate(context, keyList, cryptoInputParcel);
}
} else {
return directUpdate(context, keyList, cryptoInputParcel);
}
}
private ImportKeyResult directUpdate(Context context, ArrayList<ParcelableKeyRing> keyList,
CryptoInputParcel cryptoInputParcel) {
Log.d(Constants.TAG, "Starting normal update");
ImportOperation importOp = new ImportOperation(context, new ProviderHelper(context), null);
return importOp.execute(
new ImportKeyringParcel(keyList,
Preferences.getPreferences(context).getPreferredKeyserver()),
cryptoInputParcel
);
}
/**
* will perform a staggered update of user's keys using delays to ensure new Tor circuits, as
* performed by parcimonie. Relevant issue and method at:
* https://github.com/open-keychain/open-keychain/issues/1337
*
* @return result of the sync
*/
private ImportKeyResult staggeredUpdate(Context context, ArrayList<ParcelableKeyRing> keyList,
CryptoInputParcel cryptoInputParcel) {
Log.d(Constants.TAG, "Starting staggered update");
// final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);
final int WEEK_IN_SECONDS = 0;
ImportOperation.KeyImportAccumulator accumulator
= new ImportOperation.KeyImportAccumulator(keyList.size(), null);
for (ParcelableKeyRing keyRing : keyList) {
int waitTime;
int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size()));
if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT) {
waitTime = staggeredTime;
} else {
waitTime = ORBOT_CIRCUIT_TIMEOUT + new Random().nextInt(ORBOT_CIRCUIT_TIMEOUT);
}
Log.d(Constants.TAG, "Updating key with fingerprint " + keyRing.mExpectedFingerprint +
" with a wait time of " + waitTime + "s");
try {
Thread.sleep(waitTime * 1000);
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Exception during sleep between key updates", e);
// skip this one
continue;
}
ArrayList<ParcelableKeyRing> keyWrapper = new ArrayList<>();
keyWrapper.add(keyRing);
if (isUpdateCancelled()) {
return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED,
new OperationResult.OperationLog());
}
ImportKeyResult result =
new ImportOperation(context, new ProviderHelper(context), null, mCancelled)
.execute(
new ImportKeyringParcel(
keyWrapper,
Preferences.getPreferences(context)
.getPreferredKeyserver()
),
cryptoInputParcel
);
if (result.isPending()) {
return result;
}
accumulator.accumulateKeyImport(result);
}
return accumulator.getConsolidatedResult();
}
/**
* 1. Get keys which have been updated recently and therefore do not need to
* be updated now
* 2. Get list of all keys and filter out ones that don't need to be updated
* 3. Return keys to be updated
*
* @return list of keys that require update
*/
private ArrayList<ParcelableKeyRing> getKeysToUpdate(Context context) {
// 1. Get keys which have been updated recently and don't need to updated now
final int INDEX_UPDATED_KEYS_MASTER_KEY_ID = 0;
final int INDEX_LAST_UPDATED = 1;
// all time in seconds not milliseconds
final long CURRENT_TIME = GregorianCalendar.getInstance().getTimeInMillis() / 1000;
Cursor updatedKeysCursor = context.getContentResolver().query(
KeychainContract.UpdatedKeys.CONTENT_URI,
new String[]{
KeychainContract.UpdatedKeys.MASTER_KEY_ID,
KeychainContract.UpdatedKeys.LAST_UPDATED
},
"? - " + KeychainContract.UpdatedKeys.LAST_UPDATED + " < " + KEY_UPDATE_LIMIT,
new String[]{"" + CURRENT_TIME},
null
);
ArrayList<Long> ignoreMasterKeyIds = new ArrayList<>();
while (updatedKeysCursor.moveToNext()) {
long masterKeyId = updatedKeysCursor.getLong(INDEX_UPDATED_KEYS_MASTER_KEY_ID);
Log.d(Constants.TAG, "Keyserver sync: Ignoring {" + masterKeyId + "} last updated at {"
+ updatedKeysCursor.getLong(INDEX_LAST_UPDATED) + "}s");
ignoreMasterKeyIds.add(masterKeyId);
}
updatedKeysCursor.close();
// 2. Make a list of public keys which should be updated
final int INDEX_MASTER_KEY_ID = 0;
final int INDEX_FINGERPRINT = 1;
Cursor keyCursor = context.getContentResolver().query(
KeychainContract.KeyRings.buildUnifiedKeyRingsUri(),
new String[]{
KeychainContract.KeyRings.MASTER_KEY_ID,
KeychainContract.KeyRings.FINGERPRINT
},
null,
null,
null
);
if (keyCursor == null) {
return new ArrayList<>();
}
ArrayList<ParcelableKeyRing> keyList = new ArrayList<>();
while (keyCursor.moveToNext()) {
long keyId = keyCursor.getLong(INDEX_MASTER_KEY_ID);
if (ignoreMasterKeyIds.contains(keyId)) {
continue;
}
Log.d(Constants.TAG, "Keyserver sync: Updating {" + keyId + "}");
String fingerprint = KeyFormattingUtils
.convertFingerprintToHex(keyCursor.getBlob(INDEX_FINGERPRINT));
String hexKeyId = KeyFormattingUtils
.convertKeyIdToHex(keyId);
// we aren't updating from keybase as of now
keyList.add(new ParcelableKeyRing(fingerprint, hexKeyId, null));
}
keyCursor.close();
return keyList;
}
private boolean isUpdateCancelled() {
return mCancelled.get();
}
/**
* will cancel an update already in progress. We send an Intent to cancel it instead of simply
* modifying a static variable sync the service is running in a process that is different from
* the default application process where the UI code runs.
*
* @param context used to send an Intent to the service requesting cancellation.
*/
public static void cancelUpdates(Context context) {
Intent intent = new Intent(context, KeyserverSyncAdapterService.class);
intent.setAction(ACTION_CANCEL);
context.startService(intent);
}
private Notification getOrbotNoification(Context context) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder.setSmallIcon(R.drawable.ic_stat_notify_24dp)
.setLargeIcon(getBitmap(R.mipmap.ic_launcher, context))
.setContentTitle(context.getString(R.string.keyserver_sync_orbot_notif_title))
.setContentText(context.getString(R.string.keyserver_sync_orbot_notif_msg))
.setAutoCancel(true);
// In case the user decides to not use tor
Intent ignoreTorIntent = new Intent(context, KeyserverSyncAdapterService.class);
ignoreTorIntent.setAction(ACTION_IGNORE_TOR);
PendingIntent ignoreTorPi = PendingIntent.getService(
context,
0, // security not issue since we're giving this pending intent to Notification Manager
ignoreTorIntent,
PendingIntent.FLAG_CANCEL_CURRENT
);
builder.addAction(R.drawable.ic_stat_tor_off,
context.getString(R.string.keyserver_sync_orbot_notif_ignore),
ignoreTorPi);
Intent startOrbotIntent = new Intent(context, KeyserverSyncAdapterService.class);
startOrbotIntent.setAction(ACTION_START_ORBOT);
PendingIntent startOrbotPi = PendingIntent.getService(
context,
0, // security not issue since we're giving this pending intent to Notification Manager
startOrbotIntent,
PendingIntent.FLAG_CANCEL_CURRENT
);
builder.addAction(R.drawable.ic_stat_tor,
context.getString(R.string.keyserver_sync_orbot_notif_start),
startOrbotPi
);
builder.setContentIntent(startOrbotPi);
return builder.build();
}
public static void enableKeyserverSync(Context context) {
try {
AccountManager manager = AccountManager.get(context);
Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE);
Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE);
if (accounts.length == 0) {
if (!manager.addAccountExplicitly(account, null, null)) {
Log.e(Constants.TAG, "Adding account failed!");
}
}
// for keyserver sync
ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY,
true);
ContentResolver.addPeriodicSync(
account,
Constants.PROVIDER_AUTHORITY,
new Bundle(),
SYNC_INTERVAL
);
} catch (SecurityException e) {
Log.e(Constants.TAG, "SecurityException when adding the account", e);
Toast.makeText(context, R.string.reinstall_openkeychain, Toast.LENGTH_LONG).show();
}
}
// from de.azapps.mirakel.helper.Helpers from https://github.com/MirakelX/mirakel-android
private Bitmap getBitmap(int resId, Context context) {
int mLargeIconWidth = (int) context.getResources().getDimension(
android.R.dimen.notification_large_icon_width);
int mLargeIconHeight = (int) context.getResources().getDimension(
android.R.dimen.notification_large_icon_height);
Drawable d;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// noinspection deprecation (can't help it at this api level)
d = context.getResources().getDrawable(resId);
} else {
d = context.getDrawable(resId);
}
if (d == null) {
return null;
}
Bitmap b = Bitmap.createBitmap(mLargeIconWidth, mLargeIconHeight, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
d.setBounds(0, 0, mLargeIconWidth, mLargeIconHeight);
d.draw(c);
return b;
}
}

View File

@@ -101,8 +101,6 @@ public class PassphraseCacheService extends Service {
private static final long DEFAULT_TTL = 15; private static final long DEFAULT_TTL = 15;
private static final int NOTIFICATION_ID = 1;
private static final int MSG_PASSPHRASE_CACHE_GET_OKAY = 1; private static final int MSG_PASSPHRASE_CACHE_GET_OKAY = 1;
private static final int MSG_PASSPHRASE_CACHE_GET_KEY_NOT_FOUND = 2; private static final int MSG_PASSPHRASE_CACHE_GET_KEY_NOT_FOUND = 2;
@@ -477,7 +475,7 @@ public class PassphraseCacheService extends Service {
private void updateService() { private void updateService() {
if (mPassphraseCache.size() > 0) { if (mPassphraseCache.size() > 0) {
startForeground(NOTIFICATION_ID, getNotification()); startForeground(Constants.Notification.PASSPHRASE_CACHE, getNotification());
} else { } else {
// stop whole service if no cached passphrases remaining // stop whole service if no cached passphrases remaining
Log.d(Constants.TAG, "PassphraseCacheService: No passphrases remaining in memory, stopping service!"); Log.d(Constants.TAG, "PassphraseCacheService: No passphrases remaining in memory, stopping service!");
@@ -511,7 +509,7 @@ public class PassphraseCacheService extends Service {
private Notification getNotification() { private Notification getNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this); NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.drawable.ic_stat_notify_24dp) builder.setSmallIcon(R.drawable.ic_stat_notify_24dp)
.setLargeIcon(getBitmap(R.drawable.ic_launcher, getBaseContext())) .setLargeIcon(getBitmap(R.mipmap.ic_launcher, getBaseContext()))
.setContentTitle(getResources().getQuantityString(R.plurals.passp_cache_notif_n_keys, .setContentTitle(getResources().getQuantityString(R.plurals.passp_cache_notif_n_keys,
mPassphraseCache.size(), mPassphraseCache.size())) mPassphraseCache.size(), mPassphraseCache.size()))
.setContentText(getString(R.string.passp_cache_notif_click_to_clear)); .setContentText(getString(R.string.passp_cache_notif_click_to_clear));
@@ -603,4 +601,4 @@ public class PassphraseCacheService extends Service {
this.passphrase = passphrase; this.passphrase = passphrase;
} }
} }
} }

View File

@@ -18,7 +18,11 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import android.app.Activity; import android.app.Activity;
import android.content.ContentResolver; import android.content.ContentResolver;
@@ -47,7 +51,20 @@ public class BackupFragment extends Fragment {
private int mIndex; private int mIndex;
static final int REQUEST_REPEAT_PASSPHRASE = 1; static final int REQUEST_REPEAT_PASSPHRASE = 1;
private ExportHelper mExportHelper;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// we won't get attached to a non-fragment activity, so the cast should be safe
mExportHelper = new ExportHelper((FragmentActivity) activity);
}
@Override
public void onDetach() {
super.onDetach();
mExportHelper = null;
}
@Override @Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -80,8 +97,7 @@ public class BackupFragment extends Fragment {
} }
if (!includeSecretKeys) { if (!includeSecretKeys) {
ExportHelper exportHelper = new ExportHelper(activity); startBackup(false);
exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, false);
return; return;
} }
@@ -136,8 +152,7 @@ public class BackupFragment extends Fragment {
return; return;
} }
ExportHelper exportHelper = new ExportHelper(activity); startBackup(true);
exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true);
} }
}.execute(activity.getContentResolver()); }.execute(activity.getContentResolver());
@@ -167,8 +182,19 @@ public class BackupFragment extends Fragment {
return; return;
} }
ExportHelper exportHelper = new ExportHelper(getActivity()); startBackup(true);
exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true);
} }
} }
private void startBackup(boolean exportSecret) {
File filename;
String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
if (exportSecret) {
filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".asc");
} else {
filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".pub.asc");
}
mExportHelper.showExportKeysDialog(null, filename, exportSecret);
}
} }

View File

@@ -30,6 +30,8 @@ public class CertifyFingerprintActivity extends BaseActivity {
protected Uri mDataUri; protected Uri mDataUri;
public static final String EXTRA_ENABLE_WORD_CONFIRM = "enable_word_confirm";
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -40,6 +42,7 @@ public class CertifyFingerprintActivity extends BaseActivity {
finish(); finish();
return; return;
} }
boolean enableWordConfirm = getIntent().getBooleanExtra(EXTRA_ENABLE_WORD_CONFIRM, false);
setFullScreenDialogClose(new View.OnClickListener() { setFullScreenDialogClose(new View.OnClickListener() {
@Override @Override
@@ -50,7 +53,7 @@ public class CertifyFingerprintActivity extends BaseActivity {
Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString());
startFragment(savedInstanceState, mDataUri); startFragment(savedInstanceState, mDataUri, enableWordConfirm);
} }
@Override @Override
@@ -58,7 +61,7 @@ public class CertifyFingerprintActivity extends BaseActivity {
setContentView(R.layout.certify_fingerprint_activity); setContentView(R.layout.certify_fingerprint_activity);
} }
private void startFragment(Bundle savedInstanceState, Uri dataUri) { private void startFragment(Bundle savedInstanceState, Uri dataUri, boolean enableWordConfirm) {
// However, if we're being restored from a previous state, // However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else // then we don't need to do anything and should return or else
// we could end up with overlapping fragments. // we could end up with overlapping fragments.
@@ -67,7 +70,7 @@ public class CertifyFingerprintActivity extends BaseActivity {
} }
// Create an instance of the fragment // Create an instance of the fragment
CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri); CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri, enableWordConfirm);
// Add the fragment to the 'fragment_container' FrameLayout // Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes! // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!

View File

@@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
@@ -34,6 +35,7 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.util.ExperimentalWordConfirm;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
@@ -44,23 +46,24 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
static final int REQUEST_CERTIFY = 1; static final int REQUEST_CERTIFY = 1;
public static final String ARG_DATA_URI = "uri"; public static final String ARG_DATA_URI = "uri";
public static final String ARG_ENABLE_WORD_CONFIRM = "enable_word_confirm";
private TextView mFingerprint; private TextView mFingerprint;
private TextView mIntro;
private static final int LOADER_ID_UNIFIED = 0; private static final int LOADER_ID_UNIFIED = 0;
private Uri mDataUri; private Uri mDataUri;
private boolean mEnableWordConfirm;
private View mActionNo;
private View mActionYes;
/** /**
* Creates new instance of this fragment * Creates new instance of this fragment
*/ */
public static CertifyFingerprintFragment newInstance(Uri dataUri) { public static CertifyFingerprintFragment newInstance(Uri dataUri, boolean enableWordConfirm) {
CertifyFingerprintFragment frag = new CertifyFingerprintFragment(); CertifyFingerprintFragment frag = new CertifyFingerprintFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putParcelable(ARG_DATA_URI, dataUri); args.putParcelable(ARG_DATA_URI, dataUri);
args.putBoolean(ARG_ENABLE_WORD_CONFIRM, enableWordConfirm);
frag.setArguments(args); frag.setArguments(args);
@@ -72,18 +75,19 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
View root = super.onCreateView(inflater, superContainer, savedInstanceState); View root = super.onCreateView(inflater, superContainer, savedInstanceState);
View view = inflater.inflate(R.layout.certify_fingerprint_fragment, getContainer()); View view = inflater.inflate(R.layout.certify_fingerprint_fragment, getContainer());
mActionNo = view.findViewById(R.id.certify_fingerprint_button_no); View actionNo = view.findViewById(R.id.certify_fingerprint_button_no);
mActionYes = view.findViewById(R.id.certify_fingerprint_button_yes); View actionYes = view.findViewById(R.id.certify_fingerprint_button_yes);
mFingerprint = (TextView) view.findViewById(R.id.certify_fingerprint_fingerprint); mFingerprint = (TextView) view.findViewById(R.id.certify_fingerprint_fingerprint);
mIntro = (TextView) view.findViewById(R.id.certify_fingerprint_intro);
mActionNo.setOnClickListener(new View.OnClickListener() { actionNo.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
getActivity().finish(); getActivity().finish();
} }
}); });
mActionYes.setOnClickListener(new View.OnClickListener() { actionYes.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
certify(mDataUri); certify(mDataUri);
@@ -103,6 +107,11 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
getActivity().finish(); getActivity().finish();
return; return;
} }
mEnableWordConfirm = getArguments().getBoolean(ARG_ENABLE_WORD_CONFIRM);
if (mEnableWordConfirm) {
mIntro.setText(R.string.certify_fingerprint_text_words);
}
loadData(dataUri); loadData(dataUri);
} }
@@ -149,10 +158,13 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
switch (loader.getId()) { switch (loader.getId()) {
case LOADER_ID_UNIFIED: { case LOADER_ID_UNIFIED: {
if (data.moveToFirst()) { if (data.moveToFirst()) {
byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT);
String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob);
mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); if (mEnableWordConfirm) {
displayWordConfirm(fingerprintBlob);
} else {
displayHexConfirm(fingerprintBlob);
}
break; break;
} }
@@ -162,6 +174,19 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
setContentShown(true); setContentShown(true);
} }
private void displayHexConfirm(byte[] fingerprintBlob) {
String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob);
mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint));
}
private void displayWordConfirm(byte[] fingerprintBlob) {
String fingerprint = ExperimentalWordConfirm.getWords(getActivity(), fingerprintBlob);
mFingerprint.setTextSize(24);
mFingerprint.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
mFingerprint.setText(fingerprint);
}
/** /**
* This is called when the last Cursor provided to onLoadFinished() above is about to be closed. * This is called when the last Cursor provided to onLoadFinished() above is about to be closed.
* We need to make sure we are no longer using it. * We need to make sure we are no longer using it.

View File

@@ -83,7 +83,9 @@ public class CertifyKeyFragment
}; };
private static final int INDEX_MASTER_KEY_ID = 1; private static final int INDEX_MASTER_KEY_ID = 1;
private static final int INDEX_USER_ID = 2; private static final int INDEX_USER_ID = 2;
@SuppressWarnings("unused")
private static final int INDEX_IS_PRIMARY = 3; private static final int INDEX_IS_PRIMARY = 3;
@SuppressWarnings("unused")
private static final int INDEX_IS_REVOKED = 4; private static final int INDEX_IS_REVOKED = 4;
private MultiUserIdsAdapter mUserIdsAdapter; private MultiUserIdsAdapter mUserIdsAdapter;

View File

@@ -29,13 +29,14 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; import org.sufficientlysecure.keychain.intents.OpenKeychainIntents;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.BaseActivity;
@@ -93,7 +94,9 @@ public class DecryptActivity extends BaseActivity {
} else if (intent.hasExtra(Intent.EXTRA_TEXT)) { } else if (intent.hasExtra(Intent.EXTRA_TEXT)) {
String text = intent.getStringExtra(Intent.EXTRA_TEXT); String text = intent.getStringExtra(Intent.EXTRA_TEXT);
Uri uri = readToTempFile(text); Uri uri = readToTempFile(text);
uris.add(uri); if (uri != null) {
uris.add(uri);
}
} }
break; break;
@@ -105,7 +108,9 @@ public class DecryptActivity extends BaseActivity {
} else if (intent.hasExtra(Intent.EXTRA_TEXT)) { } else if (intent.hasExtra(Intent.EXTRA_TEXT)) {
for (String text : intent.getStringArrayListExtra(Intent.EXTRA_TEXT)) { for (String text : intent.getStringArrayListExtra(Intent.EXTRA_TEXT)) {
Uri uri = readToTempFile(text); Uri uri = readToTempFile(text);
uris.add(uri); if (uri != null) {
uris.add(uri);
}
} }
} }
@@ -139,7 +144,9 @@ public class DecryptActivity extends BaseActivity {
String text = clip.getItemAt(0).coerceToText(this).toString(); String text = clip.getItemAt(0).coerceToText(this).toString();
uri = readToTempFile(text); uri = readToTempFile(text);
} }
uris.add(uri); if (uri != null) {
uris.add(uri);
}
break; break;
} }
@@ -170,9 +177,17 @@ public class DecryptActivity extends BaseActivity {
} }
public Uri readToTempFile(String text) throws IOException { @Nullable public Uri readToTempFile(String text) throws IOException {
Uri tempFile = TemporaryStorageProvider.createFile(this); Uri tempFile = TemporaryStorageProvider.createFile(this);
OutputStream outStream = getContentResolver().openOutputStream(tempFile); OutputStream outStream = getContentResolver().openOutputStream(tempFile);
// clean up ascii armored message, fixing newlines and stuff
String cleanedText = PgpHelper.getPgpContent(text);
if (cleanedText == null) {
return null;
}
// if cleanup didn't work, just try the raw data
outStream.write(text.getBytes()); outStream.write(text.getBytes());
outStream.close(); outStream.close();
return tempFile; return tempFile;

View File

@@ -35,7 +35,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
@@ -222,18 +221,6 @@ public class DecryptListFragment
} }
} }
private void askForOutputFilename(Uri inputUri, String originalFilename, String mimeType) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
File file = new File(inputUri.getPath());
File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR;
File targetFile = new File(parentDir, originalFilename);
FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file),
getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT);
} else {
FileHelper.saveDocument(this, mimeType, originalFilename, REQUEST_CODE_OUTPUT);
}
}
@Override @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) { switch (requestCode) {
@@ -388,7 +375,7 @@ public class DecryptListFragment
onFileClick = new OnClickListener() { onFileClick = new OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
displayWithViewIntent(uri); displayWithViewIntent(uri, false);
} }
}; };
} }
@@ -413,7 +400,7 @@ public class DecryptListFragment
} }
public void displayWithViewIntent(final Uri uri) { public void displayWithViewIntent(final Uri uri, boolean share) {
Activity activity = getActivity(); Activity activity = getActivity();
if (activity == null || mCurrentInputUri != null) { if (activity == null || mCurrentInputUri != null) {
return; return;
@@ -432,51 +419,40 @@ public class DecryptListFragment
// OpenKeychain's internal viewer // OpenKeychain's internal viewer
if ("text/plain".equals(metadata.getMimeType())) { if ("text/plain".equals(metadata.getMimeType())) {
parseMime(outputUri); if (share) {
try {
String plaintext = FileHelper.readTextFromUri(activity, outputUri, result.getCharset());
// this is a significant i/o operation, use an asynctask Intent intent = new Intent(Intent.ACTION_SEND);
// new AsyncTask<Void,Void,Intent>() { intent.setType(metadata.getMimeType());
// intent.putExtra(Intent.EXTRA_TEXT, plaintext);
// @Override startActivity(intent);
// protected Intent doInBackground(Void... params) {
// } catch (IOException e) {
// Activity activity = getActivity(); Notify.create(activity, R.string.error_preparing_data, Style.ERROR).show();
// if (activity == null) { }
// return null;
// } return;
// }
// Intent intent = new Intent(Intent.ACTION_VIEW);
// intent.setDataAndType(outputUri, "text/plain"); Intent intent = new Intent(activity, DisplayTextActivity.class);
// intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setAction(Intent.ACTION_VIEW);
// return intent; intent.setDataAndType(outputUri, metadata.getMimeType());
// } intent.putExtra(DisplayTextActivity.EXTRA_METADATA, result);
// activity.startActivity(intent);
// @Override
// protected void onPostExecute(Intent intent) {
// // for result so we can possibly get a snackbar error from internal viewer
// Activity activity = getActivity();
// if (intent == null || activity == null) {
// return;
// }
//
// LabeledIntent internalIntent = new LabeledIntent(
// new Intent(intent)
// .setClass(activity, DisplayTextActivity.class)
// .putExtra(DisplayTextActivity.EXTRA_METADATA, result),
// BuildConfig.APPLICATION_ID, R.string.view_internal, R.drawable.ic_launcher);
//
// Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
// chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
// new Parcelable[] { internalIntent });
//
// activity.startActivity(chooserIntent);
// }
//
// }.execute();
} else { } else {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(outputUri, metadata.getMimeType()); Intent intent;
if (share) {
intent = new Intent(Intent.ACTION_SEND);
intent.setType(metadata.getMimeType());
intent.putExtra(Intent.EXTRA_STREAM, outputUri);
} else {
intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(outputUri, metadata.getMimeType());
}
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
@@ -486,52 +462,6 @@ public class DecryptListFragment
} }
private void parseMime(final Uri inputUri) {
CryptoOperationHelper.Callback<MimeParsingParcel, MimeParsingResult> callback
= new CryptoOperationHelper.Callback<MimeParsingParcel, MimeParsingResult>() {
@Override
public MimeParsingParcel createOperationInput() {
return new MimeParsingParcel(inputUri, null);
}
@Override
public void onCryptoOperationSuccess(MimeParsingResult result) {
handleResult(result);
}
@Override
public void onCryptoOperationCancelled() {
}
@Override
public void onCryptoOperationError(MimeParsingResult result) {
handleResult(result);
}
public void handleResult(MimeParsingResult result) {
// TODO: merge with other log
// saveKeyResult.getLog().add(result, 0);
mOutputUris = new HashMap<>(result.getTemporaryUris().size());
for (Uri tempUri : result.getTemporaryUris()) {
// TODO: use same inputUri for all?
mOutputUris.put(inputUri, tempUri);
}
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return false;
}
};
CryptoOperationHelper mimeParsingHelper = new CryptoOperationHelper<>(3, this, callback, R.string.progress_uploading);
mimeParsingHelper.cryptoOperation();
}
@Override @Override
public PgpDecryptVerifyInputParcel createOperationInput() { public PgpDecryptVerifyInputParcel createOperationInput() {
@@ -576,13 +506,17 @@ public class DecryptListFragment
intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result); intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result);
activity.startActivity(intent); activity.startActivity(intent);
return true; return true;
case R.id.decrypt_share:
displayWithViewIntent(model.mInputUri, true);
return true;
case R.id.decrypt_save: case R.id.decrypt_save:
OpenPgpMetadata metadata = result.getDecryptionMetadata(); OpenPgpMetadata metadata = result.getDecryptionMetadata();
if (metadata == null) { if (metadata == null) {
return true; return true;
} }
mCurrentInputUri = model.mInputUri; mCurrentInputUri = model.mInputUri;
askForOutputFilename(model.mInputUri, metadata.getFilename(), metadata.getMimeType()); FileHelper.saveDocument(this, metadata.getFilename(), model.mInputUri, metadata.getMimeType(),
R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, REQUEST_CODE_OUTPUT);
return true; return true;
case R.id.decrypt_delete: case R.id.decrypt_delete:
deleteFile(activity, model.mInputUri); deleteFile(activity, model.mInputUri);

View File

@@ -48,7 +48,6 @@ import org.sufficientlysecure.keychain.service.RevokeKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;

View File

@@ -280,7 +280,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
case LOADER_ID_USER_IDS: { case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri); Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri, return new CursorLoader(getActivity(), baseUri,
UserIdsAdapter.USER_IDS_PROJECTION, null, null, null); UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
} }
case LOADER_ID_SUBKEYS: { case LOADER_ID_SUBKEYS: {

View File

@@ -83,11 +83,7 @@ public class EncryptDecryptOverviewFragment extends Fragment {
mDecryptFile.setOnClickListener(new View.OnClickListener() { mDecryptFile.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { FileHelper.openDocument(EncryptDecryptOverviewFragment.this, null, "*/*", false, REQUEST_CODE_INPUT);
FileHelper.openDocument(EncryptDecryptOverviewFragment.this, "*/*", REQUEST_CODE_INPUT);
} else {
FileHelper.openFile(EncryptDecryptOverviewFragment.this, null, "*/*", REQUEST_CODE_INPUT);
}
} }
}); });

View File

@@ -18,7 +18,6 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@@ -196,13 +195,9 @@ public class EncryptFilesFragment
} }
private void addInputUri() { private void addInputUri() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { FileHelper.openDocument(EncryptFilesFragment.this, mFilesAdapter.getModelCount() == 0 ?
FileHelper.openDocument(EncryptFilesFragment.this, "*/*", true, REQUEST_CODE_INPUT); null : mFilesAdapter.getModelItem(mFilesAdapter.getModelCount() - 1).inputUri,
} else { "*/*", true, REQUEST_CODE_INPUT);
FileHelper.openFile(EncryptFilesFragment.this, mFilesAdapter.getModelCount() == 0 ?
null : mFilesAdapter.getModelItem(mFilesAdapter.getModelCount() - 1).inputUri,
"*/*", REQUEST_CODE_INPUT);
}
} }
private void addInputUri(Uri inputUri) { private void addInputUri(Uri inputUri) {
@@ -230,19 +225,8 @@ public class EncryptFilesFragment
(mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri)) (mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri))
+ (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN);
Uri inputUri = model.inputUri; Uri inputUri = model.inputUri;
saveDocumentIntent(targetName, inputUri); FileHelper.saveDocument(this, targetName, inputUri,
} R.string.title_encrypt_to_file, R.string.specify_file_to_encrypt_to, REQUEST_CODE_OUTPUT);
private void saveDocumentIntent(String targetName, Uri inputUri) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
File file = new File(inputUri.getPath());
File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR;
File targetFile = new File(parentDir, targetName);
FileHelper.saveFile(this, getString(R.string.title_encrypt_to_file),
getString(R.string.specify_file_to_encrypt_to), targetFile, REQUEST_CODE_OUTPUT);
} else {
FileHelper.saveDocument(this, "*/*", targetName, REQUEST_CODE_OUTPUT);
}
} }
public void addFile(Intent data) { public void addFile(Intent data) {

View File

@@ -64,8 +64,8 @@ public class ImportKeysFileFragment extends Fragment {
// open .asc or .gpg files // open .asc or .gpg files
// setting it to text/plain prevents Cyanogenmod's file manager from selecting asc // setting it to text/plain prevents Cyanogenmod's file manager from selecting asc
// or gpg types! // or gpg types!
FileHelper.openFile(ImportKeysFileFragment.this, Uri.fromFile(Constants.Path.APP_DIR), FileHelper.openDocument(ImportKeysFileFragment.this,
"*/*", REQUEST_CODE_FILE); Uri.fromFile(Constants.Path.APP_DIR), "*/*", false, REQUEST_CODE_FILE);
} }
}); });

View File

@@ -18,11 +18,6 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
@@ -70,16 +65,19 @@ import org.sufficientlysecure.keychain.service.ConsolidateInputParcel;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.Preferences;
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
import se.emilsjolander.stickylistheaders.StickyListHeadersListView; import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
/** /**
* Public key list with sticky list headers. It does _not_ extend ListFragment because it uses * Public key list with sticky list headers. It does _not_ extend ListFragment because it uses
* StickyListHeaders library which does not extend upon ListView. * StickyListHeaders library which does not extend upon ListView.
@@ -536,7 +534,7 @@ public class KeyListFragment extends LoaderFragment
); );
if (cursor == null) { if (cursor == null) {
Notify.create(activity, R.string.error_loading_keys, Style.ERROR); Notify.create(activity, R.string.error_loading_keys, Notify.Style.ERROR);
return; return;
} }

View File

@@ -18,11 +18,12 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcel;
import android.support.v4.app.ListFragment; import android.support.v4.app.ListFragment;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -37,19 +38,19 @@ import android.widget.ArrayAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogLevel; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogLevel;
import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel; import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import java.io.IOException;
import java.io.OutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
public class LogDisplayFragment extends ListFragment implements OnItemClickListener { public class LogDisplayFragment extends ListFragment implements OnItemClickListener {
@@ -60,6 +61,8 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe
public static final String EXTRA_RESULT = "log"; public static final String EXTRA_RESULT = "log";
protected int mTextColor; protected int mTextColor;
private Uri mLogTempFile;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -118,170 +121,40 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_log_display_export_log: case R.id.menu_log_display_export_log:
exportLog(); shareLog();
break; break;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
private void exportLog() { private void shareLog() {
showExportLogDialog(new File(Constants.Path.APP_DIR, "export.log"));
}
private void writeToLogFile(final OperationResult.OperationLog operationLog, final File f) { Activity activity = getActivity();
OperationResult.OperationLog currLog = new OperationResult.OperationLog(); if (activity == null) {
currLog.add(OperationResult.LogType.MSG_EXPORT_LOG, 0); return;
boolean error = false;
PrintWriter pw = null;
try {
pw = new PrintWriter(f);
pw.print(getPrintableOperationLog(operationLog, ""));
if (pw.checkError()) {//IOException
Log.e(Constants.TAG, "Log Export I/O Exception " + f.getAbsolutePath());
currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_WRITING, 1);
error = true;
}
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "File not found for exporting log " + f.getAbsolutePath());
currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_FOPEN, 1);
error = true;
} }
if (pw != null) {
pw.close(); String log = mResult.getLog().getPrintableOperationLog(getResources(), 0);
if (!error && pw.checkError()) {//check if it is only pw.close() which generated error
currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_WRITING, 1); // if there is no log temp file yet, create one
error = true; if (mLogTempFile == null) {
mLogTempFile = TemporaryStorageProvider.createFile(getActivity(), "openkeychain_log.txt", "text/plain");
try {
OutputStream outputStream = activity.getContentResolver().openOutputStream(mLogTempFile);
outputStream.write(log.getBytes());
} catch (IOException e) {
Notify.create(activity, R.string.error_log_share_internal, Style.ERROR).show();
return;
} }
} }
if (!error) { Intent intent = new Intent(Intent.ACTION_SEND);
currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_SUCCESS, 1); intent.putExtra(Intent.EXTRA_STREAM, mLogTempFile);
} intent.setType("text/plain");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
int opResultCode = error ? OperationResult.RESULT_ERROR : OperationResult.RESULT_OK;
OperationResult opResult = new LogExportResult(opResultCode, currLog);
opResult.createNotify(getActivity()).show();
}
/**
* returns an indented String of an entire OperationLog
*
* @param opLog log to be converted to indented, printable format
* @param basePadding padding to add at the start of all log entries, made for use with SubLogs
* @return printable, indented version of passed operationLog
*/
private String getPrintableOperationLog(OperationResult.OperationLog opLog, String basePadding) {
String log = "";
for (LogEntryParcel anOpLog : opLog) {
log += getPrintableLogEntry(anOpLog, basePadding) + "\n";
}
log = log.substring(0, log.length() - 1);//gets rid of extra new line
return log;
}
/**
* returns an indented String of a LogEntryParcel including any sub-logs it may contain
*
* @param entryParcel log entryParcel whose String representation is to be obtained
* @return indented version of passed log entryParcel in a readable format
*/
private String getPrintableLogEntry(OperationResult.LogEntryParcel entryParcel,
String basePadding) {
final String indent = " ";//4 spaces = 1 Indent level
String padding = basePadding;
for (int i = 0; i < entryParcel.mIndent; i++) {
padding += indent;
}
String logText = padding;
switch (entryParcel.mType.mLevel) {
case DEBUG:
logText += "[DEBUG]";
break;
case INFO:
logText += "[INFO]";
break;
case WARN:
logText += "[WARN]";
break;
case ERROR:
logText += "[ERROR]";
break;
case START:
logText += "[START]";
break;
case OK:
logText += "[OK]";
break;
case CANCELLED:
logText += "[CANCELLED]";
break;
}
// special case: first parameter may be a quantity
if (entryParcel.mParameters != null && entryParcel.mParameters.length > 0
&& entryParcel.mParameters[0] instanceof Integer) {
logText += getResources().getQuantityString(entryParcel.mType.getMsgId(),
(Integer) entryParcel.mParameters[0],
entryParcel.mParameters);
} else {
logText += getResources().getString(entryParcel.mType.getMsgId(),
entryParcel.mParameters);
}
if (entryParcel instanceof SubLogEntryParcel) {
OperationResult subResult = ((SubLogEntryParcel) entryParcel).getSubResult();
LogEntryParcel subEntry = subResult.getLog().getLast();
if (subEntry != null) {
//the first line of log of subResult is same as entryParcel, so replace logText
logText = getPrintableOperationLog(subResult.getLog(), padding);
}
}
return logText;
}
private void showExportLogDialog(final File exportFile) {
String title = this.getString(R.string.title_export_log);
String message = this.getString(R.string.specify_file_to_export_log_to);
FileHelper.saveFile(new FileHelper.FileDialogCallback() {
@Override
public void onFileSelected(File file, boolean checked) {
writeToLogFile(mResult.getLog(), file);
}
}, this.getActivity().getSupportFragmentManager(), title, message, exportFile, null);
}
private static class LogExportResult extends OperationResult {
public static Creator<LogExportResult> CREATOR = new Creator<LogExportResult>() {
public LogExportResult createFromParcel(final Parcel source) {
return new LogExportResult(source);
}
public LogExportResult[] newArray(final int size) {
return new LogExportResult[size];
}
};
public LogExportResult(int result, OperationLog log) {
super(result, log);
}
/**
* trivial but necessary to implement the Parcelable protocol.
*/
public LogExportResult(Parcel source) {
super(source);
}
} }
@Override @Override

View File

@@ -21,7 +21,6 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -50,15 +49,11 @@ import org.sufficientlysecure.keychain.util.Preferences;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
/** /**
* This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
* NFC devices. * NFC devices.
* <p/>
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
* NOTE: If no CryptoInputParcel is passed via EXTRA_CRYPTO_INPUT, the CryptoInputParcel is created
* internally and is NOT meant to be used by signing operations before adding signature time
*/ */
public class NfcOperationActivity extends BaseNfcActivity { public class NfcOperationActivity extends BaseNfcActivity {
@@ -84,8 +79,8 @@ public class NfcOperationActivity extends BaseNfcActivity {
@Override @Override
protected void initTheme() { protected void initTheme() {
mThemeChanger = new ThemeChanger(this); mThemeChanger = new ThemeChanger(this);
mThemeChanger.setThemes(R.style.Theme_Keychain_Light_Dialog_SecurityToken, mThemeChanger.setThemes(R.style.Theme_Keychain_Light_Dialog,
R.style.Theme_Keychain_Dark_Dialog_SecurityToken); R.style.Theme_Keychain_Dark_Dialog);
mThemeChanger.changeTheme(); mThemeChanger.changeTheme();
} }
@@ -101,13 +96,6 @@ public class NfcOperationActivity extends BaseNfcActivity {
mInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); mInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT);
if (mInputParcel == null) {
// for compatibility when used from OpenPgpService
// (or any place other than CryptoOperationHelper)
// NOTE: This CryptoInputParcel cannot be used for signing without adding signature time
mInputParcel = new CryptoInputParcel();
}
setTitle(R.string.nfc_text); setTitle(R.string.nfc_text);
vAnimator = (ViewAnimator) findViewById(R.id.view_animator); vAnimator = (ViewAnimator) findViewById(R.id.view_animator);
@@ -163,9 +151,8 @@ public class NfcOperationActivity extends BaseNfcActivity {
break; break;
} }
case NFC_SIGN: { case NFC_SIGN: {
if (mInputParcel.getSignatureTime() == null) { mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime);
mInputParcel.addSignatureTime(new Date());
}
for (int i = 0; i < mRequiredInput.mInputData.length; i++) { for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] hash = mRequiredInput.mInputData[i]; byte[] hash = mRequiredInput.mInputData[i];
int algo = mRequiredInput.mSignAlgos[i]; int algo = mRequiredInput.mSignAlgos[i];
@@ -240,7 +227,7 @@ public class NfcOperationActivity extends BaseNfcActivity {
throw new IOException("Inappropriate key flags for smart card key."); throw new IOException("Inappropriate key flags for smart card key.");
} }
// TODO: Is this really needed? // TODO: Is this really used anywhere?
mInputParcel.addCryptoData(subkeyBytes, cardSerialNumber); mInputParcel.addCryptoData(subkeyBytes, cardSerialNumber);
} }
@@ -261,15 +248,13 @@ public class NfcOperationActivity extends BaseNfcActivity {
protected void onNfcPostExecute() throws IOException { protected void onNfcPostExecute() throws IOException {
if (mServiceIntent != null) { if (mServiceIntent != null) {
// if we're triggered by OpenPgpService // if we're triggered by OpenPgpService
// save updated cryptoInputParcel in cache
CryptoInputParcelCacheService.addCryptoInputParcel(this, mServiceIntent, mInputParcel); CryptoInputParcelCacheService.addCryptoInputParcel(this, mServiceIntent, mInputParcel);
mServiceIntent.putExtra(EXTRA_CRYPTO_INPUT,
getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT));
setResult(RESULT_OK, mServiceIntent); setResult(RESULT_OK, mServiceIntent);
} else { } else {
Intent result = new Intent(); Intent result = new Intent();
// send back the CryptoInputParcel we received
result.putExtra(RESULT_CRYPTO_INPUT, mInputParcel); result.putExtra(RESULT_CRYPTO_INPUT, mInputParcel);
// send back the CryptoInputParcel we receive, to conform with the pattern in
// CryptoOperationHelper
setResult(RESULT_OK, result); setResult(RESULT_OK, result);
} }

View File

@@ -18,12 +18,22 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import android.app.ProgressDialog;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.view.ContextThemeWrapper;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.ParcelableProxy;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
@@ -34,8 +44,14 @@ import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
public class OrbotRequiredDialogActivity extends FragmentActivity public class OrbotRequiredDialogActivity extends FragmentActivity
implements OrbotHelper.DialogActions { implements OrbotHelper.DialogActions {
public static final int MESSAGE_ORBOT_STARTED = 1;
public static final int MESSAGE_ORBOT_IGNORE = 2;
public static final int MESSAGE_DIALOG_CANCEL = 3;
// if suppplied and true will start Orbot directly without showing dialog // if suppplied and true will start Orbot directly without showing dialog
public static final String EXTRA_START_ORBOT = "start_orbot"; public static final String EXTRA_START_ORBOT = "start_orbot";
// used for communicating results when triggered from a service
public static final String EXTRA_MESSENGER = "messenger";
// to provide any previous crypto input into which proxy preference is merged // to provide any previous crypto input into which proxy preference is merged
public static final String EXTRA_CRYPTO_INPUT = "extra_crypto_input"; public static final String EXTRA_CRYPTO_INPUT = "extra_crypto_input";
@@ -43,6 +59,9 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
public static final String RESULT_CRYPTO_INPUT = "result_crypto_input"; public static final String RESULT_CRYPTO_INPUT = "result_crypto_input";
private CryptoInputParcel mCryptoInputParcel; private CryptoInputParcel mCryptoInputParcel;
private Messenger mMessenger;
private ProgressDialog mShowOrbotProgressDialog;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -54,9 +73,16 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
mCryptoInputParcel = new CryptoInputParcel(); mCryptoInputParcel = new CryptoInputParcel();
} }
mMessenger = getIntent().getParcelableExtra(EXTRA_MESSENGER);
boolean startOrbotDirect = getIntent().getBooleanExtra(EXTRA_START_ORBOT, false); boolean startOrbotDirect = getIntent().getBooleanExtra(EXTRA_START_ORBOT, false);
if (startOrbotDirect) { if (startOrbotDirect) {
OrbotHelper.bestPossibleOrbotStart(this, this); ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(this);
mShowOrbotProgressDialog = new ProgressDialog(theme);
mShowOrbotProgressDialog.setTitle(R.string.progress_starting_orbot);
mShowOrbotProgressDialog.setCancelable(false);
mShowOrbotProgressDialog.show();
OrbotHelper.bestPossibleOrbotStart(this, this, false);
} else { } else {
showDialog(); showDialog();
} }
@@ -84,13 +110,32 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) { switch (requestCode) {
case OrbotHelper.START_TOR_RESULT: { case OrbotHelper.START_TOR_RESULT: {
onOrbotStarted(); // assumption that orbot was started, no way to tell for sure dismissOrbotProgressDialog();
// unfortunately, this result is returned immediately and not when Orbot is started
// 10s is approximately the longest time Orbot has taken to start
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
onOrbotStarted(); // assumption that orbot was started
}
}, 10000);
} }
} }
} }
/**
* for when Orbot is started without showing the dialog by the EXTRA_START_ORBOT intent extra
*/
private void dismissOrbotProgressDialog() {
if (mShowOrbotProgressDialog != null) {
mShowOrbotProgressDialog.dismiss();
}
}
@Override @Override
public void onOrbotStarted() { public void onOrbotStarted() {
dismissOrbotProgressDialog();
sendMessage(MESSAGE_ORBOT_STARTED);
Intent intent = new Intent(); Intent intent = new Intent();
// send back unmodified CryptoInputParcel for a retry // send back unmodified CryptoInputParcel for a retry
intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel); intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel);
@@ -100,6 +145,7 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
@Override @Override
public void onNeutralButton() { public void onNeutralButton() {
sendMessage(MESSAGE_ORBOT_IGNORE);
Intent intent = new Intent(); Intent intent = new Intent();
mCryptoInputParcel.addParcelableProxy(ParcelableProxy.getForNoProxy()); mCryptoInputParcel.addParcelableProxy(ParcelableProxy.getForNoProxy());
intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel); intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel);
@@ -109,6 +155,19 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
@Override @Override
public void onCancel() { public void onCancel() {
sendMessage(MESSAGE_DIALOG_CANCEL);
finish(); finish();
} }
private void sendMessage(int what) {
if (mMessenger != null) {
Message msg = Message.obtain();
msg.what = what;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "Could not deliver message", e);
}
}
}
} }

View File

@@ -71,6 +71,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
* internally and is NOT meant to be used by signing operations before adding a signature time * internally and is NOT meant to be used by signing operations before adding a signature time
*/ */
public class PassphraseDialogActivity extends FragmentActivity { public class PassphraseDialogActivity extends FragmentActivity {
public static final String RESULT_CRYPTO_INPUT = "result_data"; public static final String RESULT_CRYPTO_INPUT = "result_data";
public static final String EXTRA_REQUIRED_INPUT = "required_input"; public static final String EXTRA_REQUIRED_INPUT = "required_input";
@@ -261,6 +262,9 @@ public class PassphraseDialogActivity extends FragmentActivity {
case DIVERT_TO_CARD: case DIVERT_TO_CARD:
message = getString(R.string.yubikey_pin_for, userId); message = getString(R.string.yubikey_pin_for, userId);
break; break;
// special case: empty passphrase just returns the empty passphrase
case PASSPHRASE_EMPTY:
finishCaching(new Passphrase(""));
default: default:
throw new AssertionError("Unhandled SecretKeyType (should not happen)"); throw new AssertionError("Unhandled SecretKeyType (should not happen)");
} }
@@ -280,51 +284,42 @@ public class PassphraseDialogActivity extends FragmentActivity {
mPassphraseText.setText(message); mPassphraseText.setText(message);
if (keyType == CanonicalizedSecretKey.SecretKeyType.PATTERN) { // Hack to open keyboard.
// start pattern dialog and show progress circle here... // This is the only method that I found to work across all Android versions
// Intent patternActivity = new Intent(getActivity(), LockPatternActivity.class); // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/
// patternActivity.putExtra(LockPatternActivity.EXTRA_PATTERN, "123"); // Notes: * onCreateView can't be used because we want to add buttons to the dialog
// startActivityForResult(patternActivity, REQUEST_CODE_ENTER_PATTERN); // * opening in onActivityCreated does not work on Android 4.4
mInput.setVisibility(View.INVISIBLE); mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
mProgress.setVisibility(View.VISIBLE); @Override
} else { public void onFocusChange(View v, boolean hasFocus) {
// Hack to open keyboard. mPassphraseEditText.post(new Runnable() {
// This is the only method that I found to work across all Android versions @Override
// http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ public void run() {
// Notes: * onCreateView can't be used because we want to add buttons to the dialog if (getActivity() == null || mPassphraseEditText == null) {
// * opening in onActivityCreated does not work on Android 4.4 return;
mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
mPassphraseEditText.post(new Runnable() {
@Override
public void run() {
if (getActivity() == null || mPassphraseEditText == null) {
return;
}
InputMethodManager imm = (InputMethodManager) getActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT);
} }
}); InputMethodManager imm = (InputMethodManager) getActivity()
} .getSystemService(Context.INPUT_METHOD_SERVICE);
}); imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT);
mPassphraseEditText.requestFocus(); }
});
mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
mPassphraseEditText.setOnEditorActionListener(this);
if ((keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin())
|| keyType == CanonicalizedSecretKey.SecretKeyType.PIN) {
mPassphraseEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
} else {
mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
} }
});
mPassphraseEditText.requestFocus();
mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
mPassphraseEditText.setOnEditorActionListener(this);
if ((keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin())
|| keyType == CanonicalizedSecretKey.SecretKeyType.PIN) {
mPassphraseEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
} else {
mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
} }
mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
AlertDialog dialog = alert.create(); AlertDialog dialog = alert.create();
dialog.setButton(DialogInterface.BUTTON_POSITIVE, dialog.setButton(DialogInterface.BUTTON_POSITIVE,
activity.getString(R.string.btn_unlock), (DialogInterface.OnClickListener) null); activity.getString(R.string.btn_unlock), (DialogInterface.OnClickListener) null);

View File

@@ -1,577 +0,0 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import android.annotation.TargetApi;
import android.support.v7.app.AlertDialog;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.Log;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class PassphraseWizardActivity extends FragmentActivity {
//public class PassphraseWizardActivity extends FragmentActivity implements LockPatternView.OnPatternListener {
//create or authenticate
public String selectedAction;
//for lockpattern
public static char[] pattern;
private static String passphrase = "";
//nfc string
private static byte[] output = new byte[8];
public static final String CREATE_METHOD = "create";
public static final String AUTHENTICATION = "authenticate";
NfcAdapter adapter;
PendingIntent pendingIntent;
IntentFilter writeTagFilters[];
boolean writeMode;
Tag myTag;
boolean writeNFC = false;
boolean readNFC = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getActionBar() != null) {
getActionBar().setTitle(R.string.unlock_method);
}
selectedAction = getIntent().getAction();
if (savedInstanceState == null) {
SelectMethods selectMethods = new SelectMethods();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.add(R.id.fragmentContainer, selectMethods).commit();
}
setContentView(R.layout.passphrase_wizard);
adapter = NfcAdapter.getDefaultAdapter(this);
if (adapter != null) {
pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, PassphraseWizardActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
tagDetected.addCategory(Intent.CATEGORY_DEFAULT);
writeTagFilters = new IntentFilter[]{tagDetected};
}
}
public void noPassphrase(View view) {
passphrase = "";
Toast.makeText(this, R.string.no_passphrase_set, Toast.LENGTH_SHORT).show();
this.finish();
}
public void passphrase(View view) {
Passphrase passphrase = new Passphrase();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragmentContainer, passphrase).addToBackStack(null).commit();
}
public void startLockpattern(View view) {
if (getActionBar() != null) {
getActionBar().setTitle(R.string.draw_lockpattern);
}
// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction);
// LockPatternFragment lpf = LockPatternFragment.newInstance("asd");
// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit();
}
public void cancel(View view) {
this.finish();
}
public void savePassphrase(View view) {
EditText passphrase = (EditText) findViewById(R.id.passphrase);
passphrase.setError(null);
String pw = passphrase.getText().toString();
//check and save passphrase
if (selectedAction.equals(CREATE_METHOD)) {
EditText passphraseAgain = (EditText) findViewById(R.id.passphraseAgain);
passphraseAgain.setError(null);
String pwAgain = passphraseAgain.getText().toString();
if (!TextUtils.isEmpty(pw)) {
if (!TextUtils.isEmpty(pwAgain)) {
if (pw.equals(pwAgain)) {
PassphraseWizardActivity.passphrase = pw;
Toast.makeText(this, getString(R.string.passphrase_saved), Toast.LENGTH_SHORT).show();
this.finish();
} else {
passphrase.setError(getString(R.string.passphrase_invalid));
passphrase.requestFocus();
}
} else {
passphraseAgain.setError(getString(R.string.missing_passphrase));
passphraseAgain.requestFocus();
}
} else {
passphrase.setError(getString(R.string.missing_passphrase));
passphrase.requestFocus();
}
}
//check for right passphrase
if (selectedAction.equals(AUTHENTICATION)) {
if (pw.equals(PassphraseWizardActivity.passphrase)) {
Toast.makeText(this, getString(R.string.unlocked), Toast.LENGTH_SHORT).show();
this.finish();
} else {
passphrase.setError(getString(R.string.passphrase_invalid));
passphrase.requestFocus();
}
}
}
public void NFC(View view) {
if (adapter != null) {
if (getActionBar() != null) {
getActionBar().setTitle(R.string.nfc_title);
}
NFCFragment nfc = new NFCFragment();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragmentContainer, nfc).addToBackStack(null).commit();
//if you want to create a new method or just authenticate
if (CREATE_METHOD.equals(selectedAction)) {
writeNFC = true;
} else if (AUTHENTICATION.equals(selectedAction)) {
readNFC = true;
}
if (!adapter.isEnabled()) {
showAlertDialog(getString(R.string.enable_nfc), true);
}
} else {
showAlertDialog(getString(R.string.no_nfc_support), false);
}
}
@Override
protected void onNewIntent(Intent intent) {
if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
myTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (writeNFC && CREATE_METHOD.equals(selectedAction)) {
//write new password on NFC tag
try {
if (myTag != null) {
write(myTag);
writeNFC = false; //just write once
Toast.makeText(this, R.string.nfc_write_succesful, Toast.LENGTH_SHORT).show();
//advance to lockpattern
// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction);
// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit();
}
} catch (IOException | FormatException e) {
Log.e(Constants.TAG, "Failed to write on NFC tag", e);
}
} else if (readNFC && AUTHENTICATION.equals(selectedAction)) {
//read pw from NFC tag
try {
if (myTag != null) {
//if tag detected, read tag
String pwtag = read(myTag);
if (output != null && pwtag.equals(output.toString())) {
//passwort matches, go to next view
Toast.makeText(this, R.string.passphrases_match + "!", Toast.LENGTH_SHORT).show();
// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction);
// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit();
readNFC = false; //just once
} else {
//passwort doesnt match
TextView nfc = (TextView) findViewById(R.id.nfcText);
nfc.setText(R.string.nfc_wrong_tag);
}
}
} catch (IOException | FormatException e) {
Log.e(Constants.TAG, "Failed to read NFC tag", e);
}
}
}
}
private void write(Tag tag) throws IOException, FormatException {
//generate new random key and write them on the tag
SecureRandom sr = new SecureRandom();
sr.nextBytes(output);
NdefRecord[] records = {createRecord(output.toString())};
NdefMessage message = new NdefMessage(records);
Ndef ndef = Ndef.get(tag);
ndef.connect();
ndef.writeNdefMessage(message);
ndef.close();
}
private String read(Tag tag) throws IOException, FormatException {
//read string from tag
String password = null;
Ndef ndef = Ndef.get(tag);
ndef.connect();
NdefMessage ndefMessage = ndef.getCachedNdefMessage();
NdefRecord[] records = ndefMessage.getRecords();
for (NdefRecord ndefRecord : records) {
if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {
try {
password = readText(ndefRecord);
} catch (UnsupportedEncodingException e) {
Log.e(Constants.TAG, "Failed to read password from tag", e);
}
}
}
ndef.close();
return password;
}
private String readText(NdefRecord record) throws UnsupportedEncodingException {
//low-level method for reading nfc
byte[] payload = record.getPayload();
String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16";
int languageCodeLength = payload[0] & 0063;
return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding);
}
private NdefRecord createRecord(String text) throws UnsupportedEncodingException {
//low-level method for writing nfc
String lang = "en";
byte[] textBytes = text.getBytes();
byte[] langBytes = lang.getBytes("US-ASCII");
int langLength = langBytes.length;
int textLength = textBytes.length;
byte[] payload = new byte[1 + langLength + textLength];
// set status byte (see NDEF spec for actual bits)
payload[0] = (byte) langLength;
// copy langbytes and textbytes into payload
System.arraycopy(langBytes, 0, payload, 1, langLength);
System.arraycopy(textBytes, 0, payload, 1 + langLength, textLength);
return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], payload);
}
public void showAlertDialog(String message, boolean nfc) {
//This method shows an AlertDialog
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle("Information").setMessage(message).setPositiveButton("Ok",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
}
);
if (nfc) {
alert.setNeutralButton(R.string.nfc_settings,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {
startActivity(new Intent(Settings.ACTION_NFC_SETTINGS));
}
}
);
}
alert.show();
}
@Override
public void onPause() {
//pause this app and free nfc intent
super.onPause();
if (adapter != null) {
WriteModeOff();
}
}
@Override
public void onResume() {
//resume this app and get nfc intent
super.onResume();
if (adapter != null) {
WriteModeOn();
}
}
private void WriteModeOn() {
//enable nfc for this view
writeMode = true;
adapter.enableForegroundDispatch(this, pendingIntent, writeTagFilters, null);
}
private void WriteModeOff() {
//disable nfc for this view
writeMode = false;
adapter.disableForegroundDispatch(this);
}
public static class SelectMethods extends Fragment {
// private OnFragmentInteractionListener mListener;
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*/
public SelectMethods() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onResume() {
super.onResume();
if (getActivity().getActionBar() != null) {
getActivity().getActionBar().setTitle(R.string.unlock_method);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.passphrase_wizard_fragment_select_methods, container, false);
}
// @Override
// public void onAttach(Activity activity) {
// super.onAttach(activity);
// try {
// mListener = (OnFragmentInteractionListener) activity;
// } catch (ClassCastException e) {
// throw new ClassCastException(activity.toString()
// + " must implement OnFragmentInteractionListener");
// }
// }
//
// @Override
// public void onDetach() {
// super.onDetach();
// mListener = null;
// }
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
* <p/>
* See the Android Training lesson <a href=
* "http://developer.android.com/training/basics/fragments/communicating.html"
* >Communicating with Other Fragments</a> for more information.
*/
// public static interface OnFragmentInteractionListener {
// public void onFragmentInteraction(Uri uri);
// }
}
// /**
// * A simple {@link android.support.v4.app.Fragment} subclass.
// * Activities that contain this fragment must implement the
// * {@link com.haibison.android.lockpattern.Passphrase.OnFragmentInteractionListener} interface
// * to handle interaction events.
// */
public static class Passphrase extends Fragment {
// private OnFragmentInteractionListener mListener;
public Passphrase() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.passphrase_wizard_fragment_passphrase, container, false);
EditText passphraseAgain = (EditText) view.findViewById(R.id.passphraseAgain);
TextView passphraseText = (TextView) view.findViewById(R.id.passphraseText);
TextView passphraseTextAgain = (TextView) view.findViewById(R.id.passphraseTextAgain);
String selectedAction = getActivity().getIntent().getAction();
if (selectedAction.equals(AUTHENTICATION)) {
passphraseAgain.setVisibility(View.GONE);
passphraseTextAgain.setVisibility(View.GONE);
passphraseText.setText(R.string.enter_passphrase);
// getActivity().getActionBar().setTitle(R.string.enter_passphrase);
} else if (selectedAction.equals(CREATE_METHOD)) {
passphraseAgain.setVisibility(View.VISIBLE);
passphraseTextAgain.setVisibility(View.VISIBLE);
passphraseText.setText(R.string.passphrase);
// getActivity().getActionBar().setTitle(R.string.set_passphrase);
}
return view;
}
// @Override
// public void onAttach(Activity activity) {
// super.onAttach(activity);
// try {
// mListener = (OnFragmentInteractionListener) activity;
// } catch (ClassCastException e) {
// throw new ClassCastException(activity.toString()
// + " must implement OnFragmentInteractionListener");
// }
// }
//
// @Override
// public void onDetach() {
// super.onDetach();
// mListener = null;
// }
// /**
// * This interface must be implemented by activities that contain this
// * fragment to allow an interaction in this fragment to be communicated
// * to the activity and potentially other fragments contained in that
// * activity.
// * <p/>
// * See the Android Training lesson <a href=
// * "http://developer.android.com/training/basics/fragments/communicating.html"
// * >Communicating with Other Fragments</a> for more information.
// */
// public interface OnFragmentInteractionListener {
// public void onFragmentInteraction(Uri uri);
// }
}
/**
* A simple {@link android.support.v4.app.Fragment} subclass.
* Activities that contain this fragment must implement the
* interface
* to handle interaction events.
* Use the method to
* create an instance of this fragment.
*/
public static class NFCFragment extends Fragment {
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
// private OnFragmentInteractionListener mListener;
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment SelectMethods.
*/
// TODO: Rename and change types and number of parameters
public static NFCFragment newInstance(String param1, String param2) {
NFCFragment fragment = new NFCFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
public NFCFragment() {
// Required empty public constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.passphrase_wizard_fragment_nfc, container, false);
}
// // TODO: Rename method, update argument and hook method into UI event
// public void onButtonPressed(Uri uri) {
// if (mListener != null) {
// mListener.onFragmentInteraction(uri);
// }
// }
// @Override
// public void onAttach(Activity activity) {
// super.onAttach(activity);
// try {
// mListener = (OnFragmentInteractionListener) activity;
// } catch (ClassCastException e) {
// throw new ClassCastException(activity.toString()
// + " must implement OnFragmentInteractionListener");
// }
// }
// @Override
// public void onDetach() {
// super.onDetach();
// mListener = null;
// }
}
}

View File

@@ -18,11 +18,12 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import android.annotation.TargetApi; import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity; import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.CheckBoxPreference; import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference; import android.preference.EditTextPreference;
@@ -31,6 +32,8 @@ import android.preference.Preference;
import android.preference.PreferenceActivity; import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.provider.ContactsContract;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
@@ -74,7 +77,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
String action = getIntent().getAction(); String action = getIntent().getAction();
if (action != null && action.equals(ACTION_PREFS_CLOUD)) { if (ACTION_PREFS_CLOUD.equals(action)) {
addPreferencesFromResource(R.xml.cloud_search_prefs); addPreferencesFromResource(R.xml.cloud_search_prefs);
mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS); mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS);
@@ -91,14 +94,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
} }
}); });
initializeSearchKeyserver( initializeSearchKeyserver(
(CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER)
); );
initializeSearchKeybase( initializeSearchKeybase(
(CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE)
); );
} else if (action != null && action.equals(ACTION_PREFS_ADV)) { } else if (ACTION_PREFS_ADV.equals(action)) {
addPreferencesFromResource(R.xml.adv_preferences); addPreferencesFromResource(R.xml.passphrase_preferences);
initializePassphraseCacheSubs( initializePassphraseCacheSubs(
(CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS)); (CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS));
@@ -112,7 +115,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
initializeUseNumKeypadForYubiKeyPin( initializeUseNumKeypadForYubiKeyPin(
(CheckBoxPreference) findPreference(Constants.Pref.USE_NUMKEYPAD_FOR_YUBIKEY_PIN)); (CheckBoxPreference) findPreference(Constants.Pref.USE_NUMKEYPAD_FOR_YUBIKEY_PIN));
} else if (action != null && action.equals(ACTION_PREFS_GUI)) { } else if (ACTION_PREFS_GUI.equals(action)) {
addPreferencesFromResource(R.xml.gui_preferences); addPreferencesFromResource(R.xml.gui_preferences);
initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); initializeTheme((ListPreference) findPreference(Constants.Pref.THEME));
@@ -192,10 +195,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
} }
}); });
initializeSearchKeyserver( initializeSearchKeyserver(
(CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER)
); );
initializeSearchKeybase( initializeSearchKeybase(
(CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE)
); );
} }
@@ -219,14 +222,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
/** /**
* This fragment shows the PIN/password preferences * This fragment shows the PIN/password preferences
*/ */
public static class AdvancedPrefsFragment extends PreferenceFragment { public static class PassphrasePrefsFragment extends PreferenceFragment {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Load the preferences from an XML resource // Load the preferences from an XML resource
addPreferencesFromResource(R.xml.adv_preferences); addPreferencesFromResource(R.xml.passphrase_preferences);
initializePassphraseCacheSubs( initializePassphraseCacheSubs(
(CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS)); (CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS));
@@ -253,8 +256,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
} }
public static class Initializer { public static class Initializer {
private CheckBoxPreference mUseTor; private SwitchPreference mUseTor;
private CheckBoxPreference mUseNormalProxy; private SwitchPreference mUseNormalProxy;
private EditTextPreference mProxyHost; private EditTextPreference mProxyHost;
private EditTextPreference mProxyPort; private EditTextPreference mProxyPort;
private ListPreference mProxyType; private ListPreference mProxyType;
@@ -290,8 +293,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
mActivity.addPreferencesFromResource(R.xml.proxy_prefs); mActivity.addPreferencesFromResource(R.xml.proxy_prefs);
} }
mUseTor = (CheckBoxPreference) automaticallyFindPreference(Constants.Pref.USE_TOR_PROXY); mUseTor = (SwitchPreference) automaticallyFindPreference(Constants.Pref.USE_TOR_PROXY);
mUseNormalProxy = (CheckBoxPreference) automaticallyFindPreference(Constants.Pref.USE_NORMAL_PROXY); mUseNormalProxy = (SwitchPreference) automaticallyFindPreference(Constants.Pref.USE_NORMAL_PROXY);
mProxyHost = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_HOST); mProxyHost = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_HOST);
mProxyPort = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_PORT); mProxyPort = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_PORT);
mProxyType = (ListPreference) automaticallyFindPreference(Constants.Pref.PROXY_TYPE); mProxyType = (ListPreference) automaticallyFindPreference(Constants.Pref.PROXY_TYPE);
@@ -467,11 +470,121 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
} }
} }
/**
* This fragment shows the keyserver/contacts sync preferences
*/
public static class SyncPrefsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.sync_preferences);
}
@Override
public void onResume() {
super.onResume();
// this needs to be done in onResume since the user can change sync values from Android
// settings and we need to reflect that change when the user navigates back
AccountManager manager = AccountManager.get(getActivity());
final Account account = manager.getAccountsByType(Constants.ACCOUNT_TYPE)[0];
// for keyserver sync
initializeSyncCheckBox(
(SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER),
account,
Constants.PROVIDER_AUTHORITY
);
// for contacts sync
initializeSyncCheckBox(
(SwitchPreference) findPreference(Constants.Pref.SYNC_CONTACTS),
account,
ContactsContract.AUTHORITY
);
}
private void initializeSyncCheckBox(final SwitchPreference syncCheckBox,
final Account account,
final String authority) {
boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority);
syncCheckBox.setChecked(syncEnabled);
setSummary(syncCheckBox, authority, syncEnabled);
syncCheckBox.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean syncEnabled = (Boolean) newValue;
if (syncEnabled) {
ContentResolver.setSyncAutomatically(account, authority, true);
} else {
// disable syncs
ContentResolver.setSyncAutomatically(account, authority, false);
// cancel any ongoing/pending syncs
ContentResolver.cancelSync(account, authority);
}
setSummary(syncCheckBox, authority, syncEnabled);
return true;
}
});
}
private void setSummary(SwitchPreference syncCheckBox, String authority,
boolean checked) {
switch (authority) {
case Constants.PROVIDER_AUTHORITY: {
if (checked) {
syncCheckBox.setSummary(R.string.label_sync_settings_keyserver_summary_on);
} else {
syncCheckBox.setSummary(R.string.label_sync_settings_keyserver_summary_off);
}
break;
}
case ContactsContract.AUTHORITY: {
if (checked) {
syncCheckBox.setSummary(R.string.label_sync_settings_contacts_summary_on);
} else {
syncCheckBox.setSummary(R.string.label_sync_settings_contacts_summary_off);
}
break;
}
}
}
}
/**
* This fragment shows experimental features
*/
public static class ExperimentalPrefsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.experimental_preferences);
initializeExperimentalEnableWordConfirm(
(SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM));
initializeExperimentalEnableLinkedIdentities(
(SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES));
initializeExperimentalEnableKeybase(
(SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_KEYBASE));
initializeTheme((ListPreference) findPreference(Constants.Pref.THEME));
}
}
protected boolean isValidFragment(String fragmentName) { protected boolean isValidFragment(String fragmentName) {
return AdvancedPrefsFragment.class.getName().equals(fragmentName) return PassphrasePrefsFragment.class.getName().equals(fragmentName)
|| CloudSearchPrefsFragment.class.getName().equals(fragmentName) || CloudSearchPrefsFragment.class.getName().equals(fragmentName)
|| ProxyPrefsFragment.class.getName().equals(fragmentName) || ProxyPrefsFragment.class.getName().equals(fragmentName)
|| GuiPrefsFragment.class.getName().equals(fragmentName) || GuiPrefsFragment.class.getName().equals(fragmentName)
|| SyncPrefsFragment.class.getName().equals(fragmentName)
|| ExperimentalPrefsFragment.class.getName().equals(fragmentName)
|| super.isValidFragment(fragmentName); || super.isValidFragment(fragmentName);
} }
@@ -502,11 +615,13 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
private static void initializeTheme(final ListPreference mTheme) { private static void initializeTheme(final ListPreference mTheme) {
mTheme.setValue(sPreferences.getTheme()); mTheme.setValue(sPreferences.getTheme());
mTheme.setSummary(mTheme.getEntry()); mTheme.setSummary(mTheme.getEntry() + "\n"
+ mTheme.getContext().getString(R.string.label_experimental_settings_theme_summary));
mTheme.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { mTheme.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) { public boolean onPreferenceChange(Preference preference, Object newValue) {
mTheme.setValue((String) newValue); mTheme.setValue((String) newValue);
mTheme.setSummary(mTheme.getEntry()); mTheme.setSummary(mTheme.getEntry() + "\n"
+ mTheme.getContext().getString(R.string.label_experimental_settings_theme_summary));
sPreferences.setTheme((String) newValue); sPreferences.setTheme((String) newValue);
((SettingsActivity) mTheme.getContext()).recreate(); ((SettingsActivity) mTheme.getContext()).recreate();
@@ -516,7 +631,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}); });
} }
private static void initializeSearchKeyserver(final CheckBoxPreference mSearchKeyserver) { private static void initializeSearchKeyserver(final SwitchPreference mSearchKeyserver) {
Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs(); Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs();
mSearchKeyserver.setChecked(prefs.searchKeyserver); mSearchKeyserver.setChecked(prefs.searchKeyserver);
mSearchKeyserver.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { mSearchKeyserver.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@@ -529,7 +644,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}); });
} }
private static void initializeSearchKeybase(final CheckBoxPreference mSearchKeybase) { private static void initializeSearchKeybase(final SwitchPreference mSearchKeybase) {
Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs(); Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs();
mSearchKeybase.setChecked(prefs.searchKeybase); mSearchKeybase.setChecked(prefs.searchKeybase);
mSearchKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { mSearchKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@@ -571,4 +686,37 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
} }
}); });
} }
private static void initializeExperimentalEnableWordConfirm(final SwitchPreference mExperimentalEnableWordConfirm) {
mExperimentalEnableWordConfirm.setChecked(sPreferences.getExperimentalEnableWordConfirm());
mExperimentalEnableWordConfirm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mExperimentalEnableWordConfirm.setChecked((Boolean) newValue);
sPreferences.setExperimentalEnableWordConfirm((Boolean) newValue);
return false;
}
});
}
private static void initializeExperimentalEnableLinkedIdentities(final SwitchPreference mExperimentalEnableLinkedIdentities) {
mExperimentalEnableLinkedIdentities.setChecked(sPreferences.getExperimentalEnableLinkedIdentities());
mExperimentalEnableLinkedIdentities.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mExperimentalEnableLinkedIdentities.setChecked((Boolean) newValue);
sPreferences.setExperimentalEnableLinkedIdentities((Boolean) newValue);
return false;
}
});
}
private static void initializeExperimentalEnableKeybase(final SwitchPreference mExperimentalKeybase) {
mExperimentalKeybase.setChecked(sPreferences.getExperimentalEnableKeybase());
mExperimentalKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mExperimentalKeybase.setChecked((Boolean) newValue);
sPreferences.setExperimentalEnableKeybase((Boolean) newValue);
return false;
}
});
}
} }

View File

@@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.BaseActivity;
@@ -34,6 +35,19 @@ public class SettingsKeyServerActivity extends BaseActivity {
Intent intent = getIntent(); Intent intent = getIntent();
String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS); String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS);
loadFragment(savedInstanceState, servers); loadFragment(savedInstanceState, servers);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
} }
@Override @Override

View File

@@ -18,6 +18,11 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import android.animation.ArgbEvaluator; import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@@ -32,6 +37,9 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
@@ -45,14 +53,13 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener; import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.getbase.floatingactionbutton.FloatingActionButton;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
@@ -65,8 +72,10 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType;
import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
@@ -80,8 +89,6 @@ import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.NfcHelper; import org.sufficientlysecure.keychain.util.NfcHelper;
import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.Preferences;
import java.io.IOException;
import java.util.ArrayList;
public class ViewKeyActivity extends BaseNfcActivity implements public class ViewKeyActivity extends BaseNfcActivity implements
LoaderManager.LoaderCallbacks<Cursor>, LoaderManager.LoaderCallbacks<Cursor>,
@@ -97,6 +104,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements
static final int REQUEST_DELETE = 4; static final int REQUEST_DELETE = 4;
public static final String EXTRA_DISPLAY_RESULT = "display_result"; public static final String EXTRA_DISPLAY_RESULT = "display_result";
public static final String EXTRA_LINKED_TRANSITION = "linked_transition";
ProviderHelper mProviderHelper; ProviderHelper mProviderHelper;
@@ -107,16 +115,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements
private ArrayList<ParcelableKeyRing> mKeyList; private ArrayList<ParcelableKeyRing> mKeyList;
private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mOperationHelper; private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mOperationHelper;
private TextView mName;
private TextView mStatusText; private TextView mStatusText;
private ImageView mStatusImage; private ImageView mStatusImage;
private RelativeLayout mBigToolbar; private AppBarLayout mAppBarLayout;
private CollapsingToolbarLayout mCollapsingToolbarLayout;
private ImageButton mActionEncryptFile; private ImageButton mActionEncryptFile;
private ImageButton mActionEncryptText; private ImageButton mActionEncryptText;
private ImageButton mActionNfc; private ImageButton mActionNfc;
private FloatingActionButton mFab; private FloatingActionButton mFab;
private ImageView mPhoto; private ImageView mPhoto;
private FrameLayout mPhotoLayout;
private ImageView mQrCode; private ImageView mQrCode;
private CardView mQrCodeLayout; private CardView mQrCodeLayout;
@@ -156,16 +165,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements
setTitle(null); setTitle(null);
mName = (TextView) findViewById(R.id.view_key_name);
mStatusText = (TextView) findViewById(R.id.view_key_status); mStatusText = (TextView) findViewById(R.id.view_key_status);
mStatusImage = (ImageView) findViewById(R.id.view_key_status_image); mStatusImage = (ImageView) findViewById(R.id.view_key_status_image);
mBigToolbar = (RelativeLayout) findViewById(R.id.toolbar_big); mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
mCollapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
mActionEncryptFile = (ImageButton) findViewById(R.id.view_key_action_encrypt_files); mActionEncryptFile = (ImageButton) findViewById(R.id.view_key_action_encrypt_files);
mActionEncryptText = (ImageButton) findViewById(R.id.view_key_action_encrypt_text); mActionEncryptText = (ImageButton) findViewById(R.id.view_key_action_encrypt_text);
mActionNfc = (ImageButton) findViewById(R.id.view_key_action_nfc); mActionNfc = (ImageButton) findViewById(R.id.view_key_action_nfc);
mFab = (FloatingActionButton) findViewById(R.id.fab); mFab = (FloatingActionButton) findViewById(R.id.fab);
mPhoto = (ImageView) findViewById(R.id.view_key_photo); mPhoto = (ImageView) findViewById(R.id.view_key_photo);
mPhotoLayout = (FrameLayout) findViewById(R.id.view_key_photo_layout);
mQrCode = (ImageView) findViewById(R.id.view_key_qr_code); mQrCode = (ImageView) findViewById(R.id.view_key_qr_code);
mQrCodeLayout = (CardView) findViewById(R.id.view_key_qr_code_layout); mQrCodeLayout = (CardView) findViewById(R.id.view_key_qr_code_layout);
@@ -287,13 +297,26 @@ public class ViewKeyActivity extends BaseNfcActivity implements
return; return;
} }
boolean linkedTransition = getIntent().getBooleanExtra(EXTRA_LINKED_TRANSITION, false);
if (linkedTransition && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
postponeEnterTransition();
}
FragmentManager manager = getSupportFragmentManager(); FragmentManager manager = getSupportFragmentManager();
// Create an instance of the fragment // Create an instance of the fragment
final ViewKeyFragment frag = ViewKeyFragment.newInstance(mDataUri); final ViewKeyFragment frag = ViewKeyFragment.newInstance(mDataUri,
linkedTransition ? PostponeType.LINKED : PostponeType.NONE);
manager.beginTransaction() manager.beginTransaction()
.replace(R.id.view_key_fragment, frag) .replace(R.id.view_key_fragment, frag)
.commit(); .commit();
if (Preferences.getPreferences(this).getExperimentalEnableKeybase()) {
final ViewKeyKeybaseFragment keybaseFrag = ViewKeyKeybaseFragment.newInstance(mDataUri);
manager.beginTransaction()
.replace(R.id.view_key_keybase_fragment, keybaseFrag)
.commit();
}
// need to postpone loading of the yubikey fragment until after mMasterKeyId // need to postpone loading of the yubikey fragment until after mMasterKeyId
// is available, but we mark here that this should be done // is available, but we mark here that this should be done
mShowYubikeyAfterCreation = true; mShowYubikeyAfterCreation = true;
@@ -344,12 +367,23 @@ public class ViewKeyActivity extends BaseNfcActivity implements
} }
return true; return true;
} }
case R.id.menu_key_view_add_linked_identity: {
Intent intent = new Intent(this, LinkedIdWizard.class);
intent.setData(mDataUri);
startActivity(intent);
finish();
return true;
}
case R.id.menu_key_view_edit: { case R.id.menu_key_view_edit: {
editKey(mDataUri); editKey(mDataUri);
return true; return true;
} }
case R.id.menu_key_view_certify_fingerprint: { case R.id.menu_key_view_certify_fingerprint: {
certifyFingeprint(mDataUri); certifyFingeprint(mDataUri, false);
return true;
}
case R.id.menu_key_view_certify_fingerprint_word: {
certifyFingeprint(mDataUri, true);
return true; return true;
} }
} }
@@ -360,10 +394,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem editKey = menu.findItem(R.id.menu_key_view_edit); MenuItem editKey = menu.findItem(R.id.menu_key_view_edit);
editKey.setVisible(mIsSecret); editKey.setVisible(mIsSecret);
MenuItem exportKey = menu.findItem(R.id.menu_key_view_export_file); MenuItem exportKey = menu.findItem(R.id.menu_key_view_export_file);
exportKey.setVisible(mIsSecret); exportKey.setVisible(mIsSecret);
MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity);
addLinked.setVisible(mIsSecret
&& Preferences.getPreferences(this).getExperimentalEnableLinkedIdentities());
MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint); MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint);
certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked); certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked);
MenuItem certifyFingerprintWord = menu.findItem(R.id.menu_key_view_certify_fingerprint_word);
certifyFingerprintWord.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked
&& Preferences.getPreferences(this).getExperimentalEnableWordConfirm());
return true; return true;
} }
@@ -375,16 +418,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements
startActivityForResult(scanQrCode, REQUEST_QR_FINGERPRINT); startActivityForResult(scanQrCode, REQUEST_QR_FINGERPRINT);
} }
private void certifyFingeprint(Uri dataUri) { private void certifyFingeprint(Uri dataUri, boolean enableWordConfirm) {
Intent intent = new Intent(this, CertifyFingerprintActivity.class); Intent intent = new Intent(this, CertifyFingerprintActivity.class);
intent.setData(dataUri); intent.setData(dataUri);
intent.putExtra(CertifyFingerprintActivity.EXTRA_ENABLE_WORD_CONFIRM, enableWordConfirm);
startActivityForResult(intent, REQUEST_CERTIFY); startActivityForResult(intent, REQUEST_CERTIFY);
} }
private void certifyImmediate() { private void certifyImmediate() {
Intent intent = new Intent(this, CertifyKeyActivity.class); Intent intent = new Intent(this, CertifyKeyActivity.class);
intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[]{mMasterKeyId}); intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { mMasterKeyId });
startActivityForResult(intent, REQUEST_CERTIFY); startActivityForResult(intent, REQUEST_CERTIFY);
} }
@@ -413,7 +457,8 @@ public class ViewKeyActivity extends BaseNfcActivity implements
private void backupToFile() { private void backupToFile() {
new ExportHelper(this).showExportKeysDialog( new ExportHelper(this).showExportKeysDialog(
mMasterKeyId, Constants.Path.APP_DIR_FILE, true); mMasterKeyId, new File(Constants.Path.APP_DIR,
KeyFormattingUtils.convertKeyIdToHex(mMasterKeyId) + ".sec.asc"), true);
} }
private void deleteKey() { private void deleteKey() {
@@ -437,13 +482,13 @@ public class ViewKeyActivity extends BaseNfcActivity implements
return; return;
} }
if (resultCode != Activity.RESULT_OK) {
return;
}
switch (requestCode) { switch (requestCode) {
case REQUEST_QR_FINGERPRINT: { case REQUEST_QR_FINGERPRINT: {
if (resultCode != Activity.RESULT_OK) {
return;
}
// If there is an EXTRA_RESULT, that's an error. Just show it. // If there is an EXTRA_RESULT, that's an error. Just show it.
if (data.hasExtra(OperationResult.EXTRA_RESULT)) { if (data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
@@ -465,11 +510,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements
} }
case REQUEST_BACKUP: { case REQUEST_BACKUP: {
if (resultCode != Activity.RESULT_OK) {
return;
}
backupToFile(); backupToFile();
return; return;
} }
case REQUEST_CERTIFY: { case REQUEST_CERTIFY: {
if (resultCode != Activity.RESULT_OK) {
return;
}
if (data.hasExtra(OperationResult.EXTRA_RESULT)) { if (data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
result.createNotify(this).show(); result.createNotify(this).show();
@@ -478,6 +531,10 @@ public class ViewKeyActivity extends BaseNfcActivity implements
} }
case REQUEST_DELETE: { case REQUEST_DELETE: {
if (resultCode != Activity.RESULT_OK) {
return;
}
setResult(RESULT_OK, data); setResult(RESULT_OK, data);
finish(); finish();
return; return;
@@ -530,7 +587,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements
finish(); finish();
} }
}, R.string.snack_yubikey_view).show(); }, R.string.snack_yubikey_view).show();
// and if it's not found, offer import // and if it's not found, offer import
} catch (PgpKeyNotFoundException e) { } catch (PgpKeyNotFoundException e) {
Notify.create(this, R.string.snack_yubi_other, Notify.LENGTH_LONG, Notify.create(this, R.string.snack_yubi_other, Notify.LENGTH_LONG,
@@ -728,9 +784,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements
// get name, email, and comment from USER_ID // get name, email, and comment from USER_ID
KeyRing.UserId mainUserId = KeyRing.splitUserId(data.getString(INDEX_USER_ID)); KeyRing.UserId mainUserId = KeyRing.splitUserId(data.getString(INDEX_USER_ID));
if (mainUserId.name != null) { if (mainUserId.name != null) {
mName.setText(mainUserId.name); mCollapsingToolbarLayout.setTitle(mainUserId.name);
} else { } else {
mName.setText(R.string.user_id_no_name); mCollapsingToolbarLayout.setTitle(getString(R.string.user_id_no_name));
} }
mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
@@ -767,8 +823,12 @@ public class ViewKeyActivity extends BaseNfcActivity implements
} }
protected void onPostExecute(Bitmap photo) { protected void onPostExecute(Bitmap photo) {
if (photo == null) {
return;
}
mPhoto.setImageBitmap(photo); mPhoto.setImageBitmap(photo);
mPhoto.setVisibility(View.VISIBLE); mPhotoLayout.setVisibility(View.VISIBLE);
} }
}; };
@@ -781,9 +841,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements
State.REVOKED, R.color.icons, true); State.REVOKED, R.color.icons, true);
color = getResources().getColor(R.color.key_flag_red); color = getResources().getColor(R.color.key_flag_red);
mActionEncryptFile.setVisibility(View.GONE); mActionEncryptFile.setVisibility(View.INVISIBLE);
mActionEncryptText.setVisibility(View.GONE); mActionEncryptText.setVisibility(View.INVISIBLE);
mActionNfc.setVisibility(View.GONE); mActionNfc.setVisibility(View.INVISIBLE);
mFab.setVisibility(View.GONE); mFab.setVisibility(View.GONE);
mQrCodeLayout.setVisibility(View.GONE); mQrCodeLayout.setVisibility(View.GONE);
} else if (mIsExpired) { } else if (mIsExpired) {
@@ -797,9 +857,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements
State.EXPIRED, R.color.icons, true); State.EXPIRED, R.color.icons, true);
color = getResources().getColor(R.color.key_flag_red); color = getResources().getColor(R.color.key_flag_red);
mActionEncryptFile.setVisibility(View.GONE); mActionEncryptFile.setVisibility(View.INVISIBLE);
mActionEncryptText.setVisibility(View.GONE); mActionEncryptText.setVisibility(View.INVISIBLE);
mActionNfc.setVisibility(View.GONE); mActionNfc.setVisibility(View.INVISIBLE);
mFab.setVisibility(View.GONE); mFab.setVisibility(View.GONE);
mQrCodeLayout.setVisibility(View.GONE); mQrCodeLayout.setVisibility(View.GONE);
} else if (mIsSecret) { } else if (mIsSecret) {
@@ -814,15 +874,15 @@ public class ViewKeyActivity extends BaseNfcActivity implements
mQrCodeLayout.setVisibility(View.VISIBLE); mQrCodeLayout.setVisibility(View.VISIBLE);
// and place leftOf qr code // and place leftOf qr code
RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams) // RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams)
mName.getLayoutParams(); // mName.getLayoutParams();
// remove right margin // // remove right margin
nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); // nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { // if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
nameParams.setMarginEnd(0); // nameParams.setMarginEnd(0);
} // }
nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); // nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout);
mName.setLayoutParams(nameParams); // mName.setLayoutParams(nameParams);
RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams) RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams)
mStatusText.getLayoutParams(); mStatusText.getLayoutParams();
@@ -844,7 +904,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements
} }
mFab.setVisibility(View.VISIBLE); mFab.setVisibility(View.VISIBLE);
// noinspection deprecation (no getDrawable with theme at current minApi level 15!) // noinspection deprecation (no getDrawable with theme at current minApi level 15!)
mFab.setIconDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp)); mFab.setImageDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp));
} else { } else {
mActionEncryptFile.setVisibility(View.VISIBLE); mActionEncryptFile.setVisibility(View.VISIBLE);
mActionEncryptText.setVisibility(View.VISIBLE); mActionEncryptText.setVisibility(View.VISIBLE);
@@ -872,22 +932,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements
} }
if (mPreviousColor == 0 || mPreviousColor == color) { if (mPreviousColor == 0 || mPreviousColor == color) {
mStatusBar.setBackgroundColor(getStatusBarBackgroundColor(color)); mAppBarLayout.setBackgroundColor(color);
mBigToolbar.setBackgroundColor(color); mCollapsingToolbarLayout.setContentScrimColor(color);
mCollapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color));
mPreviousColor = color; mPreviousColor = color;
} else { } else {
ObjectAnimator colorFade1 = ObjectAnimator colorFade =
ObjectAnimator.ofObject(mStatusBar, "backgroundColor", ObjectAnimator.ofObject(mAppBarLayout, "backgroundColor",
new ArgbEvaluator(), mPreviousColor,
getStatusBarBackgroundColor(color));
ObjectAnimator colorFade2 =
ObjectAnimator.ofObject(mBigToolbar, "backgroundColor",
new ArgbEvaluator(), mPreviousColor, color); new ArgbEvaluator(), mPreviousColor, color);
mCollapsingToolbarLayout.setContentScrimColor(color);
mCollapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color));
colorFade1.setDuration(1200); colorFade.setDuration(1200);
colorFade2.setDuration(1200); colorFade.start();
colorFade1.start();
colorFade2.start();
mPreviousColor = color; mPreviousColor = color;
} }
@@ -963,4 +1020,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements
public boolean onCryptoSetProgress(String msg, int progress, int max) { public boolean onCryptoSetProgress(String msg, int progress, int max) {
return true; return true;
} }
} }

View File

@@ -57,7 +57,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements
public static final int TAB_IDENTITIES = 1; public static final int TAB_IDENTITIES = 1;
public static final int TAB_SUBKEYS = 2; public static final int TAB_SUBKEYS = 2;
public static final int TAB_CERTS = 3; public static final int TAB_CERTS = 3;
public static final int TAB_KEYBASE = 4;
// view // view
private ViewPager mViewPager; private ViewPager mViewPager;
@@ -140,11 +139,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements
adapter.addTab(ViewKeyAdvCertsFragment.class, adapter.addTab(ViewKeyAdvCertsFragment.class,
certsBundle, getString(R.string.key_view_tab_certs)); certsBundle, getString(R.string.key_view_tab_certs));
Bundle trustBundle = new Bundle();
trustBundle.putParcelable(ViewKeyTrustFragment.ARG_DATA_URI, dataUri);
adapter.addTab(ViewKeyTrustFragment.class,
trustBundle, getString(R.string.key_view_tab_keybase));
// update layout after operations // update layout after operations
mSlidingTabLayout.setViewPager(mViewPager); mSlidingTabLayout.setViewPager(mViewPager);
} }

View File

@@ -133,7 +133,7 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements
case LOADER_ID_USER_IDS: { case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri); Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri, return new CursorLoader(getActivity(), baseUri,
UserIdsAdapter.USER_IDS_PROJECTION, null, null, null); UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
} }
default: default:

View File

@@ -18,52 +18,73 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import java.io.IOException;
import java.util.List;
import android.annotation.TargetApi;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v7.widget.CardView; import android.support.v7.widget.CardView;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.*; import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment;
import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment;
import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.OnIdentityLoadedListener;
import org.sufficientlysecure.keychain.util.ContactHelper; import org.sufficientlysecure.keychain.util.ContactHelper;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Preferences;
import java.util.List;
public class ViewKeyFragment extends LoaderFragment implements public class ViewKeyFragment extends LoaderFragment implements
LoaderManager.LoaderCallbacks<Cursor> { LoaderManager.LoaderCallbacks<Cursor> {
public static final String ARG_DATA_URI = "uri"; public static final String ARG_DATA_URI = "uri";
public static final String ARG_POSTPONE_TYPE = "postpone_type";
private ListView mUserIds; private ListView mUserIds;
//private ListView mLinkedSystemContact; //private ListView mLinkedSystemContact;
boolean mIsSecret = false; enum PostponeType {
NONE, LINKED;
}
CardView mSystemContactCard; boolean mIsSecret = false;
LinearLayout mSystemContactLayout;
ImageView mSystemContactPicture;
TextView mSystemContactName;
private static final int LOADER_ID_UNIFIED = 0; private static final int LOADER_ID_UNIFIED = 0;
private static final int LOADER_ID_USER_IDS = 1; private static final int LOADER_ID_USER_IDS = 1;
private static final int LOADER_ID_LINKED_CONTACT = 2; private static final int LOADER_ID_LINKED_CONTACT = 2;
private static final int LOADER_ID_LINKED_IDS = 3;
private static final String LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID private static final String LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID
= "loader_linked_contact_master_key_id"; = "loader_linked_contact_master_key_id";
@@ -71,16 +92,29 @@ public class ViewKeyFragment extends LoaderFragment implements
= "loader_linked_contact_is_secret"; = "loader_linked_contact_is_secret";
private UserIdsAdapter mUserIdsAdapter; private UserIdsAdapter mUserIdsAdapter;
private LinkedIdsAdapter mLinkedIdsAdapter;
private Uri mDataUri; private Uri mDataUri;
private PostponeType mPostponeType;
private CardView mSystemContactCard;
private LinearLayout mSystemContactLayout;
private ImageView mSystemContactPicture;
private TextView mSystemContactName;
private ListView mLinkedIds;
private CardView mLinkedIdsCard;
private byte[] mFingerprint;
private TextView mLinkedIdsExpander;
/** /**
* Creates new instance of this fragment * Creates new instance of this fragment
*/ */
public static ViewKeyFragment newInstance(Uri dataUri) { public static ViewKeyFragment newInstance(Uri dataUri, PostponeType postponeType) {
ViewKeyFragment frag = new ViewKeyFragment(); ViewKeyFragment frag = new ViewKeyFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putParcelable(ARG_DATA_URI, dataUri); args.putParcelable(ARG_DATA_URI, dataUri);
args.putInt(ARG_POSTPONE_TYPE, postponeType.ordinal());
frag.setArguments(args); frag.setArguments(args);
@@ -93,6 +127,11 @@ public class ViewKeyFragment extends LoaderFragment implements
View view = inflater.inflate(R.layout.view_key_fragment, getContainer()); View view = inflater.inflate(R.layout.view_key_fragment, getContainer());
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids); mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
mLinkedIdsCard = (CardView) view.findViewById(R.id.card_linked_ids);
mLinkedIds = (ListView) view.findViewById(R.id.view_key_linked_ids);
mLinkedIdsExpander = (TextView) view.findViewById(R.id.view_key_linked_ids_expander);
mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() { mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
@@ -100,6 +139,12 @@ public class ViewKeyFragment extends LoaderFragment implements
showUserIdInfo(position); showUserIdInfo(position);
} }
}); });
mLinkedIds.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
showLinkedId(position);
}
});
mSystemContactCard = (CardView) view.findViewById(R.id.linked_system_contact_card); mSystemContactCard = (CardView) view.findViewById(R.id.linked_system_contact_card);
mSystemContactLayout = (LinearLayout) view.findViewById(R.id.system_contact_layout); mSystemContactLayout = (LinearLayout) view.findViewById(R.id.system_contact_layout);
@@ -109,6 +154,47 @@ public class ViewKeyFragment extends LoaderFragment implements
return root; return root;
} }
private void showLinkedId(final int position) {
final LinkedIdViewFragment frag;
try {
frag = mLinkedIdsAdapter.getLinkedIdFragment(mDataUri, position, mFingerprint);
} catch (IOException e) {
e.printStackTrace();
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Transition trans = TransitionInflater.from(getActivity())
.inflateTransition(R.transition.linked_id_card_trans);
// setSharedElementReturnTransition(trans);
setExitTransition(new Fade());
frag.setSharedElementEnterTransition(trans);
}
getFragmentManager().beginTransaction()
.add(R.id.view_key_fragment, frag)
.hide(frag)
.commit();
frag.setOnIdentityLoadedListener(new OnIdentityLoadedListener() {
@Override
public void onIdentityLoaded() {
new Handler().post(new Runnable() {
@Override
public void run() {
getFragmentManager().beginTransaction()
.show(frag)
.addSharedElement(mLinkedIdsCard, "card_linked_ids")
.remove(ViewKeyFragment.this)
.addToBackStack("linked_id")
.commit();
}
});
}
});
}
private void showUserIdInfo(final int position) { private void showUserIdInfo(final int position) {
if (!mIsSecret) { if (!mIsSecret) {
final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position); final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position);
@@ -129,8 +215,6 @@ public class ViewKeyFragment extends LoaderFragment implements
* Hides card if no linked system contact exists. Sets name, picture * Hides card if no linked system contact exists. Sets name, picture
* and onClickListener for the linked system contact's layout. * and onClickListener for the linked system contact's layout.
* In the case of a secret key, "me" (own profile) contact details are loaded. * In the case of a secret key, "me" (own profile) contact details are loaded.
*
* @param contactId
*/ */
private void loadLinkedSystemContact(final long contactId) { private void loadLinkedSystemContact(final long contactId) {
// contact doesn't exist, stop // contact doesn't exist, stop
@@ -188,7 +272,6 @@ public class ViewKeyFragment extends LoaderFragment implements
* ContactsContract.Contact table) * ContactsContract.Contact table)
* *
* @param contactId _ID for row in ContactsContract.Contacts table * @param contactId _ID for row in ContactsContract.Contacts table
* @param context
*/ */
private void launchContactActivity(final long contactId, Context context) { private void launchContactActivity(final long contactId, Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
@@ -202,6 +285,7 @@ public class ViewKeyFragment extends LoaderFragment implements
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); Uri dataUri = getArguments().getParcelable(ARG_DATA_URI);
mPostponeType = PostponeType.values()[getArguments().getInt(ARG_POSTPONE_TYPE, 0)];
if (dataUri == null) { if (dataUri == null) {
Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); Log.e(Constants.TAG, "Data missing. Should be Uri of key!");
getActivity().finish(); getActivity().finish();
@@ -225,12 +309,17 @@ public class ViewKeyFragment extends LoaderFragment implements
}; };
static final int INDEX_MASTER_KEY_ID = 1; static final int INDEX_MASTER_KEY_ID = 1;
@SuppressWarnings("unused")
static final int INDEX_USER_ID = 2; static final int INDEX_USER_ID = 2;
@SuppressWarnings("unused")
static final int INDEX_IS_REVOKED = 3; static final int INDEX_IS_REVOKED = 3;
@SuppressWarnings("unused")
static final int INDEX_IS_EXPIRED = 4; static final int INDEX_IS_EXPIRED = 4;
@SuppressWarnings("unused")
static final int INDEX_VERIFIED = 5; static final int INDEX_VERIFIED = 5;
static final int INDEX_HAS_ANY_SECRET = 6; static final int INDEX_HAS_ANY_SECRET = 6;
static final int INDEX_FINGERPRINT = 7; static final int INDEX_FINGERPRINT = 7;
@SuppressWarnings("unused")
static final int INDEX_HAS_ENCRYPT = 8; static final int INDEX_HAS_ENCRYPT = 8;
private static final String[] RAWCONTACT_PROJECTION = { private static final String[] RAWCONTACT_PROJECTION = {
@@ -246,21 +335,26 @@ public class ViewKeyFragment extends LoaderFragment implements
// Prepare the loaders. Either re-connect with an existing ones, // Prepare the loaders. Either re-connect with an existing ones,
// or start new ones. // or start new ones.
// TODO Is this loader the same as the one in the activity?
getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this);
} }
@Override @Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) { public Loader<Cursor> onCreateLoader(int id, Bundle args) {
setContentShown(false);
switch (id) { switch (id) {
case LOADER_ID_UNIFIED: { case LOADER_ID_UNIFIED: {
setContentShown(false, false);
Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri);
return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null);
} }
case LOADER_ID_USER_IDS:
case LOADER_ID_USER_IDS: {
return UserIdsAdapter.createLoader(getActivity(), mDataUri); return UserIdsAdapter.createLoader(getActivity(), mDataUri);
}
case LOADER_ID_LINKED_IDS: {
return LinkedIdsAdapter.createLoader(getActivity(), mDataUri);
}
//we need a separate loader for linked contact to ensure refreshing on verification //we need a separate loader for linked contact to ensure refreshing on verification
case LOADER_ID_LINKED_CONTACT: { case LOADER_ID_LINKED_CONTACT: {
@@ -310,19 +404,21 @@ public class ViewKeyFragment extends LoaderFragment implements
if (data.moveToFirst()) { if (data.moveToFirst()) {
mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0;
mFingerprint = data.getBlob(INDEX_FINGERPRINT);
long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
// load user ids after we know if it's a secret key // load user ids after we know if it's a secret key
mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null); mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null);
mUserIds.setAdapter(mUserIdsAdapter); mUserIds.setAdapter(mUserIdsAdapter);
getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); if (Preferences.getPreferences(getActivity()).getExperimentalEnableLinkedIdentities()) {
// we need to load linked contact here to prevent lag introduced by loader mLinkedIdsAdapter =
// for the linked contact new LinkedIdsAdapter(getActivity(), null, 0, mIsSecret, mLinkedIdsExpander);
long contactId = ContactHelper.findContactId( mLinkedIds.setAdapter(mLinkedIdsAdapter);
getActivity().getContentResolver(), getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this);
masterKeyId); }
loadLinkedSystemContact(contactId);
Bundle linkedContactData = new Bundle(); Bundle linkedContactData = new Bundle();
linkedContactData.putLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID, masterKeyId); linkedContactData.putLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID, masterKeyId);
@@ -336,10 +432,28 @@ public class ViewKeyFragment extends LoaderFragment implements
} }
case LOADER_ID_USER_IDS: { case LOADER_ID_USER_IDS: {
setContentShown(true, false);
mUserIdsAdapter.swapCursor(data); mUserIdsAdapter.swapCursor(data);
break; break;
} }
case LOADER_ID_LINKED_IDS: {
mLinkedIdsAdapter.swapCursor(data);
mLinkedIdsCard.setVisibility(mLinkedIdsAdapter.getCount() > 0 ? View.VISIBLE : View.GONE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mPostponeType == PostponeType.LINKED) {
mLinkedIdsCard.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@TargetApi(VERSION_CODES.LOLLIPOP)
@Override
public boolean onPreDraw() {
mLinkedIdsCard.getViewTreeObserver().removeOnPreDrawListener(this);
getActivity().startPostponedEnterTransition();
return true;
}
});
}
break;
}
case LOADER_ID_LINKED_CONTACT: { case LOADER_ID_LINKED_CONTACT: {
if (data.moveToFirst()) {// if we have a linked contact if (data.moveToFirst()) {// if we have a linked contact
long contactId = data.getLong(INDEX_CONTACT_ID); long contactId = data.getLong(INDEX_CONTACT_ID);
@@ -349,7 +463,6 @@ public class ViewKeyFragment extends LoaderFragment implements
} }
} }
setContentShown(true);
} }
/** /**
@@ -363,6 +476,11 @@ public class ViewKeyFragment extends LoaderFragment implements
mUserIdsAdapter.swapCursor(null); mUserIdsAdapter.swapCursor(null);
break; break;
} }
case LOADER_ID_LINKED_IDS: {
mLinkedIdsCard.setVisibility(View.GONE);
mLinkedIdsAdapter.swapCursor(null);
break;
}
} }
} }

View File

@@ -59,14 +59,12 @@ import java.util.ArrayList;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
public class ViewKeyTrustFragment extends LoaderFragment implements public class ViewKeyKeybaseFragment extends LoaderFragment implements
LoaderManager.LoaderCallbacks<Cursor>, LoaderManager.LoaderCallbacks<Cursor>,
CryptoOperationHelper.Callback<KeybaseVerificationParcel, KeybaseVerificationResult> { CryptoOperationHelper.Callback<KeybaseVerificationParcel, KeybaseVerificationResult> {
public static final String ARG_DATA_URI = "uri"; public static final String ARG_DATA_URI = "uri";
private View mStartSearch;
private TextView mTrustReadout;
private TextView mReportHeader; private TextView mReportHeader;
private TableLayout mProofListing; private TableLayout mProofListing;
private LayoutInflater mInflater; private LayoutInflater mInflater;
@@ -86,15 +84,25 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
private CryptoOperationHelper<KeybaseVerificationParcel, KeybaseVerificationResult> private CryptoOperationHelper<KeybaseVerificationParcel, KeybaseVerificationResult>
mKeybaseOpHelper; mKeybaseOpHelper;
/**
* Creates new instance of this fragment
*/
public static ViewKeyKeybaseFragment newInstance(Uri dataUri) {
ViewKeyKeybaseFragment frag = new ViewKeyKeybaseFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_DATA_URI, dataUri);
frag.setArguments(args);
return frag;
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View root = super.onCreateView(inflater, superContainer, savedInstanceState); View root = super.onCreateView(inflater, superContainer, savedInstanceState);
View view = inflater.inflate(R.layout.view_key_adv_keybase_fragment, getContainer()); View view = inflater.inflate(R.layout.view_key_adv_keybase_fragment, getContainer());
mInflater = inflater; mInflater = inflater;
mTrustReadout = (TextView) view.findViewById(R.id.view_key_trust_readout);
mStartSearch = view.findViewById(R.id.view_key_trust_search_cloud);
mStartSearch.setEnabled(false);
mReportHeader = (TextView) view.findViewById(R.id.view_key_trust_cloud_narrative); mReportHeader = (TextView) view.findViewById(R.id.view_key_trust_cloud_narrative);
mProofListing = (TableLayout) view.findViewById(R.id.view_key_proof_list); mProofListing = (TableLayout) view.findViewById(R.id.view_key_proof_list);
mProofVerifyHeader = view.findViewById(R.id.view_key_proof_verify_header); mProofVerifyHeader = view.findViewById(R.id.view_key_proof_verify_header);
@@ -157,85 +165,47 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
} }
boolean nothingSpecial = true; boolean nothingSpecial = true;
StringBuilder message = new StringBuilder();
// Swap the new cursor in. (The framework will take care of closing the // Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.) // old cursor once we return.)
if (data.moveToFirst()) { if (data.moveToFirst()) {
if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) {
message.append(getString(R.string.key_trust_it_is_yours)).append("\n");
nothingSpecial = false;
} else if (data.getInt(INDEX_VERIFIED) != 0) {
message.append(getString(R.string.key_trust_already_verified)).append("\n");
nothingSpecial = false;
}
// If this key is revoked, dont trust it!
if (data.getInt(INDEX_TRUST_IS_REVOKED) != 0) {
message.append(getString(R.string.key_trust_revoked)).
append(getString(R.string.key_trust_old_keys));
nothingSpecial = false;
} else {
if (data.getInt(INDEX_TRUST_IS_EXPIRED) != 0) {
// if expired, dont trust it!
message.append(getString(R.string.key_trust_expired)).
append(getString(R.string.key_trust_old_keys));
nothingSpecial = false;
}
}
if (nothingSpecial) {
message.append(getString(R.string.key_trust_maybe));
}
final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT); final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT);
final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp);
if (fingerprint != null) {
mStartSearch.setEnabled(true); startSearch(fingerprint);
mStartSearch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final Preferences.ProxyPrefs proxyPrefs =
Preferences.getPreferences(getActivity()).getProxyPrefs();
OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() {
@Override
public void onOrbotStarted() {
mStartSearch.setEnabled(false);
new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint);
}
@Override
public void onNeutralButton() {
mStartSearch.setEnabled(false);
new DescribeKey(ParcelableProxy.getForNoProxy())
.execute(fingerprint);
}
@Override
public void onCancel() {
}
};
if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) {
mStartSearch.setEnabled(false);
new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint);
}
}
});
}
} }
mTrustReadout.setText(message);
setContentShown(true); setContentShown(true);
} }
private void startSearch(final String fingerprint) {
final Preferences.ProxyPrefs proxyPrefs =
Preferences.getPreferences(getActivity()).getProxyPrefs();
OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() {
@Override
public void onOrbotStarted() {
new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint);
}
@Override
public void onNeutralButton() {
new DescribeKey(ParcelableProxy.getForNoProxy())
.execute(fingerprint);
}
@Override
public void onCancel() {
}
};
if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) {
new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint);
}
}
/** /**
* This is called when the last Cursor provided to onLoadFinished() above is about to be closed. * This is called when the last Cursor provided to onLoadFinished() above is about to be closed.
* We need to make sure we are no longer using it. * We need to make sure we are no longer using it.
@@ -299,17 +269,16 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
return new ResultPage(getString(R.string.key_trust_results_prefix), proofList); return new ResultPage(getString(R.string.key_trust_results_prefix), proofList);
} }
private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks,String proofType){ private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks, String proofType) {
//Formatting SpannableStringBuilder with String.format() causes the links to stop working. //Formatting SpannableStringBuilder with String.format() causes the links to stop working.
//This method is to insert the links while reserving the links //This method is to insert the links while reserving the links
SpannableStringBuilder ssb = new SpannableStringBuilder(); SpannableStringBuilder ssb = new SpannableStringBuilder();
ssb.append(proofType); ssb.append(proofType);
if(proofType.contains("%s")){ if (proofType.contains("%s")) {
int i = proofType.indexOf("%s"); int i = proofType.indexOf("%s");
ssb.replace(i,i+2,proofLinks); ssb.replace(i, i + 2, proofLinks);
} } else ssb.append(proofLinks);
else ssb.append(proofLinks);
return ssb; return ssb;
} }
@@ -343,7 +312,6 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence); result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence);
} }
mStartSearch.setVisibility(View.GONE);
mReportHeader.setVisibility(View.VISIBLE); mReportHeader.setVisibility(View.VISIBLE);
mProofListing.setVisibility(View.VISIBLE); mProofListing.setVisibility(View.VISIBLE);
mReportHeader.setText(result.mHeader); mReportHeader.setText(result.mHeader);
@@ -358,22 +326,35 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
text.setMovementMethod(LinkMovementMethod.getInstance()); text.setMovementMethod(LinkMovementMethod.getInstance());
mProofListing.addView(row); mProofListing.addView(row);
} }
// mSearchReport.loadDataWithBaseURL("file:///android_res/drawable/", s, "text/html", "UTF-8", null);
} }
} }
private String getProofNarrative(int proofType) { private String getProofNarrative(int proofType) {
int stringIndex; int stringIndex;
switch (proofType) { switch (proofType) {
case Proof.PROOF_TYPE_TWITTER: stringIndex = R.string.keybase_narrative_twitter; break; case Proof.PROOF_TYPE_TWITTER:
case Proof.PROOF_TYPE_GITHUB: stringIndex = R.string.keybase_narrative_github; break; stringIndex = R.string.keybase_narrative_twitter;
case Proof.PROOF_TYPE_DNS: stringIndex = R.string.keybase_narrative_dns; break; break;
case Proof.PROOF_TYPE_WEB_SITE: stringIndex = R.string.keybase_narrative_web_site; break; case Proof.PROOF_TYPE_GITHUB:
case Proof.PROOF_TYPE_HACKERNEWS: stringIndex = R.string.keybase_narrative_hackernews; break; stringIndex = R.string.keybase_narrative_github;
case Proof.PROOF_TYPE_COINBASE: stringIndex = R.string.keybase_narrative_coinbase; break; break;
case Proof.PROOF_TYPE_REDDIT: stringIndex = R.string.keybase_narrative_reddit; break; case Proof.PROOF_TYPE_DNS:
default: stringIndex = R.string.keybase_narrative_unknown; stringIndex = R.string.keybase_narrative_dns;
break;
case Proof.PROOF_TYPE_WEB_SITE:
stringIndex = R.string.keybase_narrative_web_site;
break;
case Proof.PROOF_TYPE_HACKERNEWS:
stringIndex = R.string.keybase_narrative_hackernews;
break;
case Proof.PROOF_TYPE_COINBASE:
stringIndex = R.string.keybase_narrative_coinbase;
break;
case Proof.PROOF_TYPE_REDDIT:
stringIndex = R.string.keybase_narrative_reddit;
break;
default:
stringIndex = R.string.keybase_narrative_unknown;
} }
return getActivity().getString(stringIndex); return getActivity().getString(stringIndex);
} }
@@ -390,14 +371,22 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
// which proofs do we have working verifiers for? // which proofs do we have working verifiers for?
private boolean haveProofFor(int proofType) { private boolean haveProofFor(int proofType) {
switch (proofType) { switch (proofType) {
case Proof.PROOF_TYPE_TWITTER: return true; case Proof.PROOF_TYPE_TWITTER:
case Proof.PROOF_TYPE_GITHUB: return true; return true;
case Proof.PROOF_TYPE_DNS: return true; case Proof.PROOF_TYPE_GITHUB:
case Proof.PROOF_TYPE_WEB_SITE: return true; return true;
case Proof.PROOF_TYPE_HACKERNEWS: return true; case Proof.PROOF_TYPE_DNS:
case Proof.PROOF_TYPE_COINBASE: return true; return true;
case Proof.PROOF_TYPE_REDDIT: return true; case Proof.PROOF_TYPE_WEB_SITE:
default: return false; return true;
case Proof.PROOF_TYPE_HACKERNEWS:
return true;
case Proof.PROOF_TYPE_COINBASE:
return true;
case Proof.PROOF_TYPE_REDDIT:
return true;
default:
return false;
} }
} }

View File

@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Date;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@@ -193,9 +194,9 @@ public class KeyAdapter extends CursorAdapter {
String dateTime = DateUtils.formatDateTime(context, String dateTime = DateUtils.formatDateTime(context,
item.mCreation.getTime(), item.mCreation.getTime(),
DateUtils.FORMAT_SHOW_DATE DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH); | DateUtils.FORMAT_ABBREV_MONTH);
mCreationDate.setText(context.getString(R.string.label_key_created, mCreationDate.setText(context.getString(R.string.label_key_created,
dateTime)); dateTime));
mCreationDate.setTextColor(textColor); mCreationDate.setTextColor(textColor);

View File

@@ -0,0 +1,238 @@
/*
* Copyright (C) 2014-2015 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.adapter;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.support.v4.content.CursorLoader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.LinkedAttribute;
import org.sufficientlysecure.keychain.linked.UriAttribute;
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker;
import org.sufficientlysecure.keychain.util.FilterCursorWrapper;
import java.io.IOException;
import java.util.WeakHashMap;
public class LinkedIdsAdapter extends UserAttributesAdapter {
private final boolean mIsSecret;
protected LayoutInflater mInflater;
WeakHashMap<Integer,UriAttribute> mLinkedIdentityCache = new WeakHashMap<>();
private Cursor mUnfilteredCursor;
private TextView mExpander;
public LinkedIdsAdapter(Context context, Cursor c, int flags,
boolean isSecret, TextView expander) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
mIsSecret = isSecret;
if (expander != null) {
expander.setVisibility(View.GONE);
/* don't show an expander (maybe in some sort of advanced view?)
mExpander = expander;
mExpander.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showUnfiltered();
}
});
*/
}
}
@Override
public Cursor swapCursor(Cursor cursor) {
if (cursor == null) {
mUnfilteredCursor = null;
return super.swapCursor(null);
}
mUnfilteredCursor = cursor;
FilterCursorWrapper filteredCursor = new FilterCursorWrapper(cursor) {
@Override
public boolean isVisible(Cursor cursor) {
UriAttribute id = getItemAtPosition(cursor);
return id instanceof LinkedAttribute;
}
};
if (mExpander != null) {
int hidden = filteredCursor.getHiddenCount();
if (hidden == 0) {
mExpander.setVisibility(View.GONE);
} else {
mExpander.setVisibility(View.VISIBLE);
mExpander.setText(mContext.getResources().getQuantityString(
R.plurals.linked_id_expand, hidden));
}
}
return super.swapCursor(filteredCursor);
}
private void showUnfiltered() {
mExpander.setVisibility(View.GONE);
super.swapCursor(mUnfilteredCursor);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder holder = (ViewHolder) view.getTag();
if (!mIsSecret) {
int isVerified = cursor.getInt(INDEX_VERIFIED);
switch (isVerified) {
case Certs.VERIFIED_SECRET:
KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
break;
case Certs.VERIFIED_SELF:
KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
break;
default:
KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
break;
}
}
UriAttribute id = getItemAtPosition(cursor);
holder.setData(mContext, id);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
view.setTransitionName(id.mUri.toString());
}
}
public UriAttribute getItemAtPosition(Cursor cursor) {
int rank = cursor.getInt(INDEX_RANK);
Log.d(Constants.TAG, "requested rank: " + rank);
UriAttribute ret = mLinkedIdentityCache.get(rank);
if (ret != null) {
Log.d(Constants.TAG, "cached!");
return ret;
}
Log.d(Constants.TAG, "not cached!");
try {
byte[] data = cursor.getBlob(INDEX_ATTRIBUTE_DATA);
ret = LinkedAttribute.fromAttributeData(data);
mLinkedIdentityCache.put(rank, ret);
return ret;
} catch (IOException e) {
Log.e(Constants.TAG, "could not read linked identity subpacket data", e);
return null;
}
}
@Override
public UriAttribute getItem(int position) {
Cursor cursor = getCursor();
cursor.moveToPosition(position);
return getItemAtPosition(cursor);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View v = mInflater.inflate(R.layout.linked_id_item, null);
ViewHolder holder = new ViewHolder(v);
v.setTag(holder);
return v;
}
// don't show revoked user ids, irrelevant for average users
public static final String LINKED_IDS_WHERE = UserPackets.IS_REVOKED + " = 0";
public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
return new CursorLoader(activity, baseUri,
UserIdsAdapter.USER_PACKETS_PROJECTION, LINKED_IDS_WHERE, null, null);
}
public LinkedIdViewFragment getLinkedIdFragment(Uri baseUri,
int position, byte[] fingerprint) throws IOException {
Cursor c = getCursor();
c.moveToPosition(position);
int rank = c.getInt(UserIdsAdapter.INDEX_RANK);
Uri dataUri = UserPackets.buildLinkedIdsUri(baseUri);
return LinkedIdViewFragment.newInstance(dataUri, rank, mIsSecret, fingerprint);
}
public static class ViewHolder {
final public ImageView vVerified;
final public ImageView vIcon;
final public TextView vTitle;
final public TextView vComment;
public ViewHolder(View view) {
vVerified = (ImageView) view.findViewById(R.id.linked_id_certified_icon);
vIcon = (ImageView) view.findViewById(R.id.linked_id_type_icon);
vTitle = (TextView) view.findViewById(R.id.linked_id_title);
vComment = (TextView) view.findViewById(R.id.linked_id_comment);
}
public void setData(Context context, UriAttribute id) {
vTitle.setText(id.getDisplayTitle(context));
String comment = id.getDisplayComment(context);
if (comment != null) {
vComment.setVisibility(View.VISIBLE);
vComment.setText(comment);
} else {
vComment.setVisibility(View.GONE);
}
vIcon.setImageResource(id.getDisplayIcon());
}
public void seekAttention() {
ObjectAnimator anim = SubtleAttentionSeeker.tintText(vComment, 1000);
anim.setStartDelay(200);
anim.start();
}
}
}

View File

@@ -0,0 +1,57 @@
package org.sufficientlysecure.keychain.ui.adapter;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.support.v4.content.CursorLoader;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
public class LinkedIdsCertAdapter extends CursorAdapter {
public static final String[] USER_CERTS_PROJECTION = new String[]{
UserPackets._ID,
UserPackets.TYPE,
UserPackets.USER_ID,
UserPackets.ATTRIBUTE_DATA,
UserPackets.RANK,
UserPackets.VERIFIED,
UserPackets.IS_PRIMARY,
UserPackets.IS_REVOKED
};
protected static final int INDEX_ID = 0;
protected static final int INDEX_TYPE = 1;
protected static final int INDEX_USER_ID = 2;
protected static final int INDEX_ATTRIBUTE_DATA = 3;
protected static final int INDEX_RANK = 4;
protected static final int INDEX_VERIFIED = 5;
protected static final int INDEX_IS_PRIMARY = 6;
protected static final int INDEX_IS_REVOKED = 7;
public LinkedIdsCertAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return null;
}
public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
return new CursorLoader(activity, baseUri,
UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
}

View File

@@ -171,7 +171,7 @@ public class MultiUserIdsAdapter extends CursorAdapter {
CertifyAction action = actions.get(keyId); CertifyAction action = actions.get(keyId);
if (actions.get(keyId) == null) { if (actions.get(keyId) == null) {
actions.put(keyId, new CertifyAction(keyId, uids)); actions.put(keyId, new CertifyAction(keyId, uids, null));
} else { } else {
action.mUserIds.addAll(uids); action.mUserIds.addAll(uids);
} }

View File

@@ -137,9 +137,9 @@ abstract public class SelectKeyCursorAdapter extends CursorAdapter {
String dateTime = DateUtils.formatDateTime(context, String dateTime = DateUtils.formatDateTime(context,
cursor.getLong(mIndexCreation) * 1000, cursor.getLong(mIndexCreation) * 1000,
DateUtils.FORMAT_SHOW_DATE DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH); | DateUtils.FORMAT_ABBREV_MONTH);
h.creation.setText(context.getString(R.string.label_key_created, dateTime)); h.creation.setText(context.getString(R.string.label_key_created, dateTime));
h.creation.setVisibility(View.VISIBLE); h.creation.setVisibility(View.VISIBLE);
} else { } else {

View File

@@ -8,22 +8,24 @@ import android.view.View;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
public abstract class UserAttributesAdapter extends CursorAdapter { public abstract class UserAttributesAdapter extends CursorAdapter {
public static final String[] USER_IDS_PROJECTION = new String[]{ public static final String[] USER_PACKETS_PROJECTION = new String[]{
UserPackets._ID, UserPackets._ID,
UserPackets.TYPE, UserPackets.TYPE,
UserPackets.USER_ID, UserPackets.USER_ID,
UserPackets.ATTRIBUTE_DATA,
UserPackets.RANK, UserPackets.RANK,
UserPackets.VERIFIED, UserPackets.VERIFIED,
UserPackets.IS_PRIMARY, UserPackets.IS_PRIMARY,
UserPackets.IS_REVOKED UserPackets.IS_REVOKED
}; };
protected static final int INDEX_ID = 0; public static final int INDEX_ID = 0;
protected static final int INDEX_TYPE = 1; public static final int INDEX_TYPE = 1;
protected static final int INDEX_USER_ID = 2; public static final int INDEX_USER_ID = 2;
protected static final int INDEX_RANK = 3; public static final int INDEX_ATTRIBUTE_DATA = 3;
protected static final int INDEX_VERIFIED = 4; public static final int INDEX_RANK = 4;
protected static final int INDEX_IS_PRIMARY = 5; public static final int INDEX_VERIFIED = 5;
protected static final int INDEX_IS_REVOKED = 6; public static final int INDEX_IS_PRIMARY = 6;
public static final int INDEX_IS_REVOKED = 7;
public UserAttributesAdapter(Context context, Cursor c, int flags) { public UserAttributesAdapter(Context context, Cursor c, int flags) {
super(context, c, flags); super(context, c, flags);
@@ -46,4 +48,5 @@ public abstract class UserAttributesAdapter extends CursorAdapter {
mCursor.moveToPosition(position); mCursor.moveToPosition(position);
return mCursor.getInt(INDEX_VERIFIED); return mCursor.getInt(INDEX_VERIFIED);
} }
} }

View File

@@ -188,7 +188,7 @@ public class UserIdsAdapter extends UserAttributesAdapter {
public static CursorLoader createLoader(Activity activity, Uri dataUri) { public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildUserIdsUri(dataUri); Uri baseUri = UserPackets.buildUserIdsUri(dataUri);
return new CursorLoader(activity, baseUri, return new CursorLoader(activity, baseUri,
UserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_WHERE, null, null); UserIdsAdapter.USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null);
} }
} }

View File

@@ -30,6 +30,7 @@ import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
/** /**
@@ -51,6 +52,7 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
KeyserverSyncAdapterService.cancelUpdates(this);
if (mThemeChanger.changeTheme()) { if (mThemeChanger.changeTheme()) {
Intent intent = getIntent(); Intent intent = getIntent();

View File

@@ -18,9 +18,9 @@
package org.sufficientlysecure.keychain.ui.base; package org.sufficientlysecure.keychain.ui.base;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
@@ -50,17 +50,21 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
* @see KeychainService * @see KeychainService
* *
*/ */
abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult> public abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult>
extends Fragment implements CryptoOperationHelper.Callback<T, S> { extends Fragment implements CryptoOperationHelper.Callback<T, S> {
final private CryptoOperationHelper<T, S> mOperationHelper; final private CryptoOperationHelper<T, S> mOperationHelper;
public CryptoOperationFragment() {
mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing);
}
public CryptoOperationFragment(Integer initialProgressMsg) { public CryptoOperationFragment(Integer initialProgressMsg) {
mOperationHelper = new CryptoOperationHelper<>(1, this, this, initialProgressMsg); mOperationHelper = new CryptoOperationHelper<>(1, this, this, initialProgressMsg);
} }
public CryptoOperationFragment() { public CryptoOperationFragment(int id, Integer initialProgressMsg) {
mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing); mOperationHelper = new CryptoOperationHelper<>(id, this, this, initialProgressMsg);
} }
@Override @Override

View File

@@ -19,6 +19,8 @@
package org.sufficientlysecure.keychain.ui.base; package org.sufficientlysecure.keychain.ui.base;
import java.util.Date;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Intent; import android.content.Intent;
@@ -70,9 +72,11 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
// particular helper. a request code looks as follows: // particular helper. a request code looks as follows:
// (id << 9) + (1<<8) + REQUEST_CODE_X // (id << 9) + (1<<8) + REQUEST_CODE_X
// that is, starting from LSB, there are 8 bits request code, 1 // that is, starting from LSB, there are 8 bits request code, 1
// fixed bit set, then 7 bit operator-id code. the first two // fixed bit set, then 7 bit helper-id code. the first two
// summands are stored in the mId for easy operation. // summands are stored in the mHelperId for easy operation.
private final int mId; private final int mHelperId;
// bitmask for helperId is everything except the least 8 bits
public static final int HELPER_ID_BITMASK = ~0xff;
public static final int REQUEST_CODE_PASSPHRASE = 1; public static final int REQUEST_CODE_PASSPHRASE = 1;
public static final int REQUEST_CODE_NFC = 2; public static final int REQUEST_CODE_NFC = 2;
@@ -92,7 +96,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
*/ */
public CryptoOperationHelper(int id, FragmentActivity activity, Callback<T, S> callback, public CryptoOperationHelper(int id, FragmentActivity activity, Callback<T, S> callback,
Integer progressMessageString) { Integer progressMessageString) {
mId = (id << 9) + (1<<8); mHelperId = (id << 9) + (1<<8);
mActivity = activity; mActivity = activity;
mUseFragment = false; mUseFragment = false;
mCallback = callback; mCallback = callback;
@@ -103,7 +107,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
* if OperationHelper is being integrated into a fragment * if OperationHelper is being integrated into a fragment
*/ */
public CryptoOperationHelper(int id, Fragment fragment, Callback<T, S> callback, Integer progressMessageString) { public CryptoOperationHelper(int id, Fragment fragment, Callback<T, S> callback, Integer progressMessageString) {
mId = (id << 9) + (1<<8); mHelperId = (id << 9) + (1<<8);
mFragment = fragment; mFragment = fragment;
mUseFragment = true; mUseFragment = true;
mProgressMessageResource = progressMessageString; mProgressMessageResource = progressMessageString;
@@ -162,9 +166,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
protected void startActivityForResult(Intent intent, int requestCode) { protected void startActivityForResult(Intent intent, int requestCode) {
if (mUseFragment) { if (mUseFragment) {
mFragment.startActivityForResult(intent, mId + requestCode); mFragment.startActivityForResult(intent, mHelperId + requestCode);
} else { } else {
mActivity.startActivityForResult(intent, mId + requestCode); mActivity.startActivityForResult(intent, mHelperId + requestCode);
} }
} }
@@ -176,13 +180,13 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(Constants.TAG, "received activity result in OperationHelper"); Log.d(Constants.TAG, "received activity result in OperationHelper");
if ((requestCode & mId) != mId) { if ((requestCode & HELPER_ID_BITMASK) != mHelperId) {
// this wasn't meant for us to handle // this wasn't meant for us to handle
return false; return false;
} }
Log.d(Constants.TAG, "handling activity result in OperationHelper"); Log.d(Constants.TAG, "handling activity result in OperationHelper");
// filter out mId from requestCode // filter out mHelperId from requestCode
requestCode ^= mId; requestCode ^= mHelperId;
if (resultCode == Activity.RESULT_CANCELED) { if (resultCode == Activity.RESULT_CANCELED) {
mCallback.onCryptoOperationCancelled(); mCallback.onCryptoOperationCancelled();
@@ -313,7 +317,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
} }
public void cryptoOperation() { public void cryptoOperation() {
cryptoOperation(new CryptoInputParcel()); cryptoOperation(new CryptoInputParcel(new Date()));
} }
public void onHandleResult(OperationResult result) { public void onHandleResult(OperationResult result) {

View File

@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Message; import android.os.Message;
import android.os.Messenger; import android.os.Messenger;
@@ -119,14 +120,19 @@ public class FileDialogFragment extends DialogFragment {
mFilename = (EditText) view.findViewById(R.id.input); mFilename = (EditText) view.findViewById(R.id.input);
mFilename.setText(mFile.getName()); mFilename.setText(mFile.getName());
mBrowse = (ImageButton) view.findViewById(R.id.btn_browse); mBrowse = (ImageButton) view.findViewById(R.id.btn_browse);
mBrowse.setOnClickListener(new View.OnClickListener() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
public void onClick(View v) { mBrowse.setVisibility(View.GONE);
// only .asc or .gpg files } else {
// setting it to text/plain prevents Cynaogenmod's file manager from selecting asc mBrowse.setOnClickListener(new View.OnClickListener() {
// or gpg types! public void onClick(View v) {
FileHelper.openFile(FileDialogFragment.this, Uri.fromFile(mFile), "*/*", REQUEST_CODE); // only .asc or .gpg files
} // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
}); // or gpg types!
FileHelper.saveDocumentKitKat(
FileDialogFragment.this, "*/*", mFile.getName(), REQUEST_CODE);
}
});
}
mCheckBox = (CheckBox) view.findViewById(R.id.checkbox); mCheckBox = (CheckBox) view.findViewById(R.id.checkbox);
if (checkboxText == null) { if (checkboxText == null) {

View File

@@ -0,0 +1,225 @@
package org.sufficientlysecure.keychain.ui.linked;
import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ViewAnimator;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.LinkedAttribute;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.util.Notify;
public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragment {
protected LinkedIdWizard mLinkedIdWizard;
private ImageView mVerifyImage;
private TextView mVerifyStatus;
private ViewAnimator mVerifyAnimator;
// This is a resource, set AFTER it has been verified
LinkedTokenResource mVerifiedResource = null;
private ViewAnimator mVerifyButtonAnimator;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
protected abstract View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
@Override @NonNull
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = newView(inflater, container, savedInstanceState);
View nextButton = view.findViewById(R.id.next_button);
if (nextButton != null) {
nextButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
cryptoOperation();
}
});
}
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
mVerifyAnimator = (ViewAnimator) view.findViewById(R.id.verify_progress);
mVerifyImage = (ImageView) view.findViewById(R.id.verify_image);
mVerifyStatus = (TextView) view.findViewById(R.id.verify_status);
mVerifyButtonAnimator = (ViewAnimator) view.findViewById(R.id.verify_buttons);
view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofVerify();
}
});
view.findViewById(R.id.button_retry).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofVerify();
}
});
setVerifyProgress(false, null);
mVerifyStatus.setText(R.string.linked_verify_pending);
return view;
}
abstract LinkedTokenResource getResource(OperationLog log);
private void setVerifyProgress(boolean on, Boolean success) {
if (success == null) {
mVerifyStatus.setText(R.string.linked_verifying);
displayButton(on ? 2 : 0);
} else if (success) {
mVerifyStatus.setText(R.string.linked_verify_success);
mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout_24dp);
mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark),
PorterDuff.Mode.SRC_IN);
displayButton(2);
} else {
mVerifyStatus.setText(R.string.linked_verify_error);
mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout_24dp);
mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark),
PorterDuff.Mode.SRC_IN);
displayButton(1);
}
mVerifyAnimator.setDisplayedChild(on ? 1 : 0);
}
public void displayButton(int button) {
if (mVerifyButtonAnimator.getDisplayedChild() == button) {
return;
}
mVerifyButtonAnimator.setDisplayedChild(button);
}
protected void proofVerify() {
setVerifyProgress(true, null);
new AsyncTask<Void,Void,LinkedVerifyResult>() {
@Override
protected LinkedVerifyResult doInBackground(Void... params) {
long timer = System.currentTimeMillis();
OperationLog log = new OperationLog();
LinkedTokenResource resource = getResource(log);
if (resource == null) {
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
}
LinkedVerifyResult result = resource.verify(getActivity(), mLinkedIdWizard.mFingerprint);
// ux flow: this operation should take at last a second
timer = System.currentTimeMillis() -timer;
if (timer < 1000) try {
Thread.sleep(1000 -timer);
} catch (InterruptedException e) {
// never mind
}
if (result.success()) {
mVerifiedResource = resource;
}
return result;
}
@Override
protected void onPostExecute(LinkedVerifyResult result) {
super.onPostExecute(result);
if (result.success()) {
setVerifyProgress(false, true);
} else {
setVerifyProgress(false, false);
// on error, show error message
result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this);
}
}
}.execute();
}
@Override
protected void cryptoOperation() {
if (mVerifiedResource == null) {
Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR)
.show(LinkedIdCreateFinalFragment.this);
return;
}
super.cryptoOperation();
}
@Override
protected void cryptoOperation(CryptoInputParcel cryptoInput) {
if (mVerifiedResource == null) {
Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR)
.show(LinkedIdCreateFinalFragment.this);
return;
}
super.cryptoOperation(cryptoInput);
}
@Nullable
@Override
public Parcelable createOperationInput() {
SaveKeyringParcel skp =
new SaveKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint);
WrappedUserAttribute ua =
LinkedAttribute.fromResource(mVerifiedResource).toUserAttribute();
skp.mAddUserAttribute.add(ua);
return skp;
}
@Override
public void onCryptoOperationSuccess(OperationResult result) {
// if bad -> display here!
if (!result.success()) {
result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this);
return;
}
getActivity().finish();
}
@Override
public void onCryptoOperationError(OperationResult result) {
}
}

View File

@@ -0,0 +1,706 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.linked;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.util.Random;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.app.FragmentActivity;
import android.util.Base64;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ViewAnimator;
import javax.net.ssl.HttpsURLConnection;
import org.json.JSONException;
import org.json.JSONObject;
import org.spongycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.LinkedAttribute;
import org.sufficientlysecure.keychain.linked.resources.GithubResource;
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.ui.widget.StatusIndicator;
import org.sufficientlysecure.keychain.ui.widget.StatusIndicator.Status;
import org.sufficientlysecure.keychain.util.Log;
public class LinkedIdCreateGithubFragment extends CryptoOperationFragment<SaveKeyringParcel,EditKeyResult> {
public static final String ARG_GITHUB_COOKIE = "github_cookie";
private Button mRetryButton;
enum State {
IDLE, AUTH_PROCESS, AUTH_ERROR, POST_PROCESS, POST_ERROR, LID_PROCESS, LID_ERROR, DONE
}
ViewAnimator mButtonContainer;
StatusIndicator mStatus1, mStatus2, mStatus3;
byte[] mFingerprint;
long mMasterKeyId;
private SaveKeyringParcel mSaveKeyringParcel;
private TextView mLinkedIdTitle, mLinkedIdComment;
private boolean mFinishOnStop;
public static LinkedIdCreateGithubFragment newInstance() {
return new LinkedIdCreateGithubFragment();
}
public LinkedIdCreateGithubFragment() {
super(null);
}
@Override @NonNull
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.linked_create_github_fragment, container, false);
mButtonContainer = (ViewAnimator) view.findViewById(R.id.button_container);
mStatus1 = (StatusIndicator) view.findViewById(R.id.linked_status_step1);
mStatus2 = (StatusIndicator) view.findViewById(R.id.linked_status_step2);
mStatus3 = (StatusIndicator) view.findViewById(R.id.linked_status_step3);
mRetryButton = (Button) view.findViewById(R.id.button_retry);
((ImageView) view.findViewById(R.id.linked_id_type_icon)).setImageResource(R.drawable.linked_github);
((ImageView) view.findViewById(R.id.linked_id_certified_icon)).setImageResource(R.drawable.octo_link_24dp);
mLinkedIdTitle = (TextView) view.findViewById(R.id.linked_id_title);
mLinkedIdComment = (TextView) view.findViewById(R.id.linked_id_comment);
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdWizard activity = (LinkedIdWizard) getActivity();
if (activity == null) {
return;
}
activity.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
step1GetOAuthCode();
// for animation testing
// onCryptoOperationSuccess(null);
}
});
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
LinkedIdWizard wizard = (LinkedIdWizard) getActivity();
mFingerprint = wizard.mFingerprint;
mMasterKeyId = wizard.mMasterKeyId;
}
private void step1GetOAuthCode() {
setState(State.AUTH_PROCESS);
mButtonContainer.setDisplayedChild(1);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
oAuthRequest("github.com/login/oauth/authorize", BuildConfig.GITHUB_CLIENT_ID, "gist");
}
}, 300);
}
private void showRetryForOAuth() {
mRetryButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
v.setOnClickListener(null);
step1GetOAuthCode();
}
});
mButtonContainer.setDisplayedChild(3);
}
private void step1GetOAuthToken() {
if (mOAuthCode == null) {
setState(State.AUTH_ERROR);
showRetryForOAuth();
return;
}
Activity activity = getActivity();
if (activity == null) {
return;
}
final String gistText = GithubResource.generate(activity, mFingerprint);
new AsyncTask<Void,Void,JSONObject>() {
Exception mException;
@Override
protected JSONObject doInBackground(Void... dummy) {
try {
JSONObject params = new JSONObject();
params.put("client_id", BuildConfig.GITHUB_CLIENT_ID);
params.put("client_secret", BuildConfig.GITHUB_CLIENT_SECRET);
params.put("code", mOAuthCode);
params.put("state", mOAuthState);
return jsonHttpRequest("https://github.com/login/oauth/access_token", params, null);
} catch (IOException | HttpResultException e) {
mException = e;
} catch (JSONException e) {
throw new AssertionError("json error, this is a bug!");
}
return null;
}
@Override
protected void onPostExecute(JSONObject result) {
super.onPostExecute(result);
Activity activity = getActivity();
if (activity == null) {
// we couldn't show an error anyways
return;
}
Log.d(Constants.TAG, "response: " + result);
if (result == null || result.optString("access_token", null) == null) {
setState(State.AUTH_ERROR);
showRetryForOAuth();
if (result != null) {
Notify.create(activity, R.string.linked_error_auth_failed, Style.ERROR).show();
return;
}
if (mException instanceof SocketTimeoutException) {
Notify.create(activity, R.string.linked_error_timeout, Style.ERROR).show();
} else if (mException instanceof HttpResultException) {
Notify.create(activity, activity.getString(R.string.linked_error_http,
((HttpResultException) mException).mResponse),
Style.ERROR).show();
} else if (mException instanceof IOException) {
Notify.create(activity, R.string.linked_error_network, Style.ERROR).show();
}
return;
}
step2PostGist(result.optString("access_token"), gistText);
}
}.execute();
}
private void step2PostGist(final String accessToken, final String gistText) {
setState(State.POST_PROCESS);
new AsyncTask<Void,Void,JSONObject>() {
Exception mException;
@Override
protected JSONObject doInBackground(Void... dummy) {
try {
long timer = System.currentTimeMillis();
JSONObject file = new JSONObject();
file.put("content", gistText);
JSONObject files = new JSONObject();
files.put("openpgp.txt", file);
JSONObject params = new JSONObject();
params.put("public", true);
params.put("description", getString(R.string.linked_gist_description));
params.put("files", files);
JSONObject result = jsonHttpRequest("https://api.github.com/gists", params, accessToken);
// ux flow: this operation should take at last a second
timer = System.currentTimeMillis() -timer;
if (timer < 1000) try {
Thread.sleep(1000 -timer);
} catch (InterruptedException e) {
// never mind
}
return result;
} catch (IOException | HttpResultException e) {
mException = e;
} catch (JSONException e) {
throw new AssertionError("json error, this is a bug!");
}
return null;
}
@Override
protected void onPostExecute(JSONObject result) {
super.onPostExecute(result);
Log.d(Constants.TAG, "response: " + result);
Activity activity = getActivity();
if (activity == null) {
// we couldn't show an error anyways
return;
}
if (result == null) {
setState(State.POST_ERROR);
showRetryForOAuth();
if (mException instanceof SocketTimeoutException) {
Notify.create(activity, R.string.linked_error_timeout, Style.ERROR).show();
} else if (mException instanceof HttpResultException) {
Notify.create(activity, activity.getString(R.string.linked_error_http,
((HttpResultException) mException).mResponse),
Style.ERROR).show();
} else if (mException instanceof IOException) {
Notify.create(activity, R.string.linked_error_network, Style.ERROR).show();
}
return;
}
GithubResource resource;
try {
String gistId = result.getString("id");
JSONObject owner = result.getJSONObject("owner");
String gistLogin = owner.getString("login");
URI uri = URI.create("https://gist.github.com/" + gistLogin + "/" + gistId);
resource = GithubResource.create(uri);
} catch (JSONException e) {
setState(State.POST_ERROR);
return;
}
View linkedItem = mButtonContainer.getChildAt(2);
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
linkedItem.setTransitionName(resource.toUri().toString());
}
// we only need authorization for this one operation, drop it afterwards
revokeToken(accessToken);
step3EditKey(resource);
}
}.execute();
}
private void revokeToken(final String token) {
new AsyncTask<Void,Void,Void>() {
@Override
protected Void doInBackground(Void... dummy) {
try {
HttpsURLConnection nection = (HttpsURLConnection) new URL(
"https://api.github.com/applications/" + BuildConfig.GITHUB_CLIENT_ID + "/tokens/" + token)
.openConnection();
nection.setRequestMethod("DELETE");
String encoded = Base64.encodeToString(
(BuildConfig.GITHUB_CLIENT_ID + ":" + BuildConfig.GITHUB_CLIENT_SECRET).getBytes(), Base64.DEFAULT);
nection.setRequestProperty("Authorization", "Basic " + encoded);
nection.connect();
} catch (IOException e) {
// nvm
}
return null;
}
}.execute();
}
private void step3EditKey(final GithubResource resource) {
// set item data while we're there
{
Context context = getActivity();
mLinkedIdTitle.setText(resource.getDisplayTitle(context));
mLinkedIdComment.setText(resource.getDisplayComment(context));
}
setState(State.LID_PROCESS);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
WrappedUserAttribute ua = LinkedAttribute.fromResource(resource).toUserAttribute();
mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint);
mSaveKeyringParcel.mAddUserAttribute.add(ua);
cryptoOperation();
}
}, 250);
}
@Nullable
@Override
public SaveKeyringParcel createOperationInput() {
// if this is null, the cryptoOperation silently aborts - which is what we want in that case
return mSaveKeyringParcel;
}
@Override
public void onCryptoOperationSuccess(EditKeyResult result) {
setState(State.DONE);
mButtonContainer.getInAnimation().setDuration(750);
mButtonContainer.setDisplayedChild(2);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
FragmentActivity activity = getActivity();
Intent intent = new Intent(activity, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(mMasterKeyId));
// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
intent.putExtra(ViewKeyActivity.EXTRA_LINKED_TRANSITION, true);
View linkedItem = mButtonContainer.getChildAt(2);
Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(
activity, linkedItem, linkedItem.getTransitionName()).toBundle();
activity.startActivity(intent, options);
mFinishOnStop = true;
} else {
activity.startActivity(intent);
activity.finish();
}
}
}, 1000);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// cookies are automatically saved, we don't want that
CookieManager cookieManager = CookieManager.getInstance();
String cookie = cookieManager.getCookie("https://github.com/");
outState.putString(ARG_GITHUB_COOKIE, cookie);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null) {
String cookie = savedInstanceState.getString(ARG_GITHUB_COOKIE);
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setCookie("https://github.com/", cookie);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
try {
// cookies are automatically saved, we don't want that
CookieManager cookieManager = CookieManager.getInstance();
// noinspection deprecation (replacement is api lvl 21)
cookieManager.removeAllCookie();
} catch (Exception e) {
// no biggie if this fails
}
}
@Override
public void onStop() {
super.onStop();
if (mFinishOnStop) {
Activity activity = getActivity();
activity.setResult(Activity.RESULT_OK);
activity.finish();
}
}
@Override
public void onCryptoOperationError(EditKeyResult result) {
result.createNotify(getActivity()).show(this);
setState(State.LID_ERROR);
}
@Override
public void onCryptoOperationCancelled() {
mRetryButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
v.setOnClickListener(null);
mButtonContainer.setDisplayedChild(1);
setState(State.LID_PROCESS);
cryptoOperation();
}
});
mButtonContainer.setDisplayedChild(3);
setState(State.LID_ERROR);
}
private String mOAuthCode, mOAuthState;
public void oAuthRequest(String hostAndPath, String clientId, String scope) {
Activity activity = getActivity();
if (activity == null) {
return;
}
byte[] buf = new byte[16];
new Random().nextBytes(buf);
mOAuthState = new String(Hex.encode(buf));
mOAuthCode = null;
final Dialog auth_dialog = new Dialog(activity);
auth_dialog.setContentView(R.layout.oauth_webview);
WebView web = (WebView) auth_dialog.findViewById(R.id.web_view);
web.getSettings().setSaveFormData(false);
web.getSettings().setUserAgentString("OpenKeychain " + BuildConfig.VERSION_NAME);
web.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri = Uri.parse(url);
if ("oauth-openkeychain".equals(uri.getScheme())) {
if (mOAuthCode != null) {
return true;
}
if (uri.getQueryParameter("error") != null) {
Log.i(Constants.TAG, "got oauth error: " + uri.getQueryParameter("error"));
auth_dialog.dismiss();
return true;
}
// check if mOAuthState == queryParam[state]
mOAuthCode = uri.getQueryParameter("code");
auth_dialog.dismiss();
return true;
}
// don't surf away from github!
if (!"github.com".equals(uri.getHost())) {
auth_dialog.dismiss();
return true;
}
return false;
}
});
auth_dialog.setTitle(R.string.linked_webview_title_github);
auth_dialog.setCancelable(true);
auth_dialog.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
step1GetOAuthToken();
}
});
auth_dialog.show();
web.loadUrl("https://" + hostAndPath +
"?client_id=" + clientId +
"&scope=" + scope +
"&redirect_uri=oauth-openkeychain://linked/" +
"&state=" + mOAuthState);
}
public void setState(State state) {
switch (state) {
case IDLE:
mStatus1.setDisplayedChild(Status.IDLE);
mStatus2.setDisplayedChild(Status.IDLE);
mStatus3.setDisplayedChild(Status.IDLE);
break;
case AUTH_PROCESS:
mStatus1.setDisplayedChild(Status.PROGRESS);
mStatus2.setDisplayedChild(Status.IDLE);
mStatus3.setDisplayedChild(Status.IDLE);
break;
case AUTH_ERROR:
mStatus1.setDisplayedChild(Status.ERROR);
mStatus2.setDisplayedChild(Status.IDLE);
mStatus3.setDisplayedChild(Status.IDLE);
break;
case POST_PROCESS:
mStatus1.setDisplayedChild(Status.OK);
mStatus2.setDisplayedChild(Status.PROGRESS);
mStatus3.setDisplayedChild(Status.IDLE);
break;
case POST_ERROR:
mStatus1.setDisplayedChild(Status.OK);
mStatus2.setDisplayedChild(Status.ERROR);
mStatus3.setDisplayedChild(Status.IDLE);
break;
case LID_PROCESS:
mStatus1.setDisplayedChild(Status.OK);
mStatus2.setDisplayedChild(Status.OK);
mStatus3.setDisplayedChild(Status.PROGRESS);
break;
case LID_ERROR:
mStatus1.setDisplayedChild(Status.OK);
mStatus2.setDisplayedChild(Status.OK);
mStatus3.setDisplayedChild(Status.ERROR);
break;
case DONE:
mStatus1.setDisplayedChild(Status.OK);
mStatus2.setDisplayedChild(Status.OK);
mStatus3.setDisplayedChild(Status.OK);
}
}
private static JSONObject jsonHttpRequest(String url, JSONObject params, String accessToken)
throws IOException, HttpResultException {
HttpsURLConnection nection = (HttpsURLConnection) new URL(url).openConnection();
nection.setDoInput(true);
nection.setDoOutput(true);
nection.setConnectTimeout(2000);
nection.setReadTimeout(1000);
nection.setRequestProperty("Content-Type", "application/json");
nection.setRequestProperty("Accept", "application/json");
nection.setRequestProperty("User-Agent", "OpenKeychain " + BuildConfig.VERSION_NAME);
if (accessToken != null) {
nection.setRequestProperty("Authorization", "token " + accessToken);
}
OutputStream os = nection.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
writer.write(params.toString());
writer.flush();
writer.close();
os.close();
try {
nection.connect();
int code = nection.getResponseCode();
if (code != HttpsURLConnection.HTTP_CREATED && code != HttpsURLConnection.HTTP_OK) {
throw new HttpResultException(nection.getResponseCode(), nection.getResponseMessage());
}
InputStream in = new BufferedInputStream(nection.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
response.append(line);
}
try {
return new JSONObject(response.toString());
} catch (JSONException e) {
throw new IOException(e);
}
} finally {
nection.disconnect();
}
}
static class HttpResultException extends Exception {
final int mCode;
final String mResponse;
HttpResultException(int code, String response) {
mCode = code;
mResponse = response;
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.linked;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Patterns;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
public class LinkedIdCreateHttpsStep1Fragment extends Fragment {
LinkedIdWizard mLinkedIdWizard;
EditText mEditUri;
public static LinkedIdCreateHttpsStep1Fragment newInstance() {
LinkedIdCreateHttpsStep1Fragment frag = new LinkedIdCreateHttpsStep1Fragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.linked_create_https_fragment_step1, container, false);
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String uri = "https://" + mEditUri.getText();
if (!checkUri(uri)) {
return;
}
String proofText = GenericHttpsResource.generateText(getActivity(),
mLinkedIdWizard.mFingerprint);
LinkedIdCreateHttpsStep2Fragment frag =
LinkedIdCreateHttpsStep2Fragment.newInstance(uri, proofText);
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
mEditUri.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void afterTextChanged(Editable editable) {
String uri = "https://" + editable;
if (uri.length() > 0) {
if (checkUri(uri)) {
mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
R.drawable.ic_stat_retyped_ok, 0);
} else {
mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
R.drawable.ic_stat_retyped_bad, 0);
}
} else {
// remove drawable if email is empty
mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
});
// mEditUri.setText("mugenguild.com/pgpkey.txt");
return view;
}
private static boolean checkUri(String uri) {
return Patterns.WEB_URL.matcher(uri).matches();
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.linked;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FileHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragment {
private static final int REQUEST_CODE_OUTPUT = 0x00007007;
public static final String ARG_URI = "uri", ARG_TEXT = "text";
EditText mEditUri;
URI mResourceUri;
String mResourceString;
public static LinkedIdCreateHttpsStep2Fragment newInstance
(String uri, String proofText) {
LinkedIdCreateHttpsStep2Fragment frag = new LinkedIdCreateHttpsStep2Fragment();
Bundle args = new Bundle();
args.putString(ARG_URI, uri);
args.putString(ARG_TEXT, proofText);
frag.setArguments(args);
return frag;
}
@Override
GenericHttpsResource getResource(OperationLog log) {
return GenericHttpsResource.createNew(mResourceUri);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
mResourceUri = new URI(getArguments().getString(ARG_URI));
} catch (URISyntaxException e) {
e.printStackTrace();
getActivity().finish();
}
mResourceString = getArguments().getString(ARG_TEXT);
}
protected View newView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.linked_create_https_fragment_step2, container, false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (view != null) {
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofSend();
}
});
view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofSave();
}
});
mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
mEditUri.setText(mResourceUri.toString());
}
return view;
}
private void proofSend () {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
sendIntent.setType("text/plain");
startActivity(sendIntent);
}
private void proofSave () {
String state = Environment.getExternalStorageState();
if (!Environment.MEDIA_MOUNTED.equals(state)) {
Notify.create(getActivity(), "External storage not available!", Style.ERROR);
return;
}
String targetName = "pgpkey.txt";
FileHelper.saveDocument(this,
targetName, Uri.fromFile(new File(Constants.Path.APP_DIR, targetName)),
"text/plain", R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to,
REQUEST_CODE_OUTPUT);
}
private void saveFile(Uri uri) {
try {
PrintWriter out =
new PrintWriter(getActivity().getContentResolver().openOutputStream(uri));
out.print(mResourceString);
if (out.checkError()) {
Notify.create(getActivity(), "Error writing file!", Style.ERROR).show();
}
} catch (FileNotFoundException e) {
Notify.create(getActivity(), "File could not be opened for writing!", Style.ERROR).show();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
// For saving a file
case REQUEST_CODE_OUTPUT:
if (data == null) {
return;
}
Uri uri = data.getData();
saveFile(uri);
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.linked;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.util.Notify;
public class LinkedIdCreateTwitterStep1Fragment extends Fragment {
LinkedIdWizard mLinkedIdWizard;
EditText mEditHandle;
public static LinkedIdCreateTwitterStep1Fragment newInstance() {
LinkedIdCreateTwitterStep1Fragment frag = new LinkedIdCreateTwitterStep1Fragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step1, container, false);
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
final String handle = mEditHandle.getText().toString();
if ("".equals(handle)) {
mEditHandle.setError("Please input a Twitter handle!");
return;
}
new AsyncTask<Void,Void,Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
return true;
// return checkHandle(handle);
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result == null) {
Notify.create(getActivity(),
"Connection error while checking username!",
Notify.Style.ERROR).show(LinkedIdCreateTwitterStep1Fragment.this);
return;
}
if (!result) {
Notify.create(getActivity(),
"This handle does not exist on Twitter!",
Notify.Style.ERROR).show(LinkedIdCreateTwitterStep1Fragment.this);
return;
}
LinkedIdCreateTwitterStep2Fragment frag =
LinkedIdCreateTwitterStep2Fragment.newInstance(handle);
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
}.execute();
}
});
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
}
});
mEditHandle = (EditText) view.findViewById(R.id.linked_create_twitter_handle);
return view;
}
/* not used at this point, too many problems
private static Boolean checkHandle(String handle) {
try {
HttpURLConnection nection =
(HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection();
nection.setRequestMethod("HEAD");
nection.setRequestProperty("User-Agent", "OpenKeychain");
return nection.getResponseCode() == 200;
} catch (IOException e) {
return null;
}
}
*/
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.linked;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.linked.resources.TwitterResource;
public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragment {
public static final String ARG_HANDLE = "handle";
String mResourceHandle;
String mResourceString;
public static LinkedIdCreateTwitterStep2Fragment newInstance
(String handle) {
LinkedIdCreateTwitterStep2Fragment frag = new LinkedIdCreateTwitterStep2Fragment();
Bundle args = new Bundle();
args.putString(ARG_HANDLE, handle);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mResourceString =
TwitterResource.generate(mLinkedIdWizard.mFingerprint);
mResourceHandle = getArguments().getString(ARG_HANDLE);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (view != null) {
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofSend();
}
});
view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
proofShare();
}
});
((TextView) view.findViewById(R.id.linked_tweet_published)).setText(
Html.fromHtml(getString(R.string.linked_create_twitter_2_3, mResourceHandle))
);
}
return view;
}
@Override
LinkedTokenResource getResource(OperationLog log) {
return TwitterResource.searchInTwitterStream(getActivity(),
mResourceHandle, mResourceString, log);
}
@Override
protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.linked_create_twitter_fragment_step2, container, false);
}
private void proofShare() {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
sendIntent.setType("text/plain");
startActivity(sendIntent);
}
private void proofSend() {
Uri.Builder builder = Uri.parse("https://twitter.com/intent/tweet").buildUpon();
builder.appendQueryParameter("text", mResourceString);
Uri uri = builder.build();
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
getActivity().startActivity(intent);
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.linked;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.sufficientlysecure.keychain.R;
public class LinkedIdSelectFragment extends Fragment {
LinkedIdWizard mLinkedIdWizard;
/**
* Creates new instance of this fragment
*/
public static LinkedIdSelectFragment newInstance() {
LinkedIdSelectFragment frag = new LinkedIdSelectFragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.linked_select_fragment, container, false);
view.findViewById(R.id.linked_create_https_button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateHttpsStep1Fragment frag =
LinkedIdCreateHttpsStep1Fragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
/*
view.findViewById(R.id.linked_create_dns_button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateDnsStep1Fragment frag =
LinkedIdCreateDnsStep1Fragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
*/
view.findViewById(R.id.linked_create_twitter_button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateTwitterStep1Fragment frag =
LinkedIdCreateTwitterStep1Fragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
view.findViewById(R.id.linked_create_github_button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateGithubFragment frag =
LinkedIdCreateGithubFragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}
});
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLinkedIdWizard = (LinkedIdWizard) getActivity();
}
}

View File

@@ -0,0 +1,560 @@
package org.sufficientlysecure.keychain.ui.linked;
import java.io.IOException;
import java.util.Collections;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextSwitcher;
import android.widget.TextView;
import android.widget.ViewAnimator;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.key;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
import org.sufficientlysecure.keychain.linked.LinkedAttribute;
import org.sufficientlysecure.keychain.linked.LinkedResource;
import org.sufficientlysecure.keychain.linked.UriAttribute;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.ViewHolder.VerifyState;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker;
import org.sufficientlysecure.keychain.ui.widget.CertListWidget;
import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner;
import org.sufficientlysecure.keychain.util.Log;
public class LinkedIdViewFragment extends CryptoOperationFragment implements
LoaderManager.LoaderCallbacks<Cursor>, OnBackStackChangedListener {
private static final String ARG_DATA_URI = "data_uri";
private static final String ARG_LID_RANK = "rank";
private static final String ARG_IS_SECRET = "verified";
private static final String ARG_FINGERPRINT = "fingerprint";
private static final int LOADER_ID_LINKED_ID = 1;
private UriAttribute mLinkedId;
private LinkedTokenResource mLinkedResource;
private boolean mIsSecret;
private Context mContext;
private byte[] mFingerprint;
private AsyncTask mInProgress;
private Uri mDataUri;
private ViewHolder mViewHolder;
private int mLidRank;
private OnIdentityLoadedListener mIdLoadedListener;
private long mCertifyKeyId;
public static LinkedIdViewFragment newInstance(Uri dataUri, int rank,
boolean isSecret, byte[] fingerprint) throws IOException {
LinkedIdViewFragment frag = new LinkedIdViewFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_DATA_URI, dataUri);
args.putInt(ARG_LID_RANK, rank);
args.putBoolean(ARG_IS_SECRET, isSecret);
args.putByteArray(ARG_FINGERPRINT, fingerprint);
frag.setArguments(args);
return frag;
}
public LinkedIdViewFragment() {
// IMPORTANT: the id must be unique in the ViewKeyActivity CryptoOperationHelper id namespace!
// no initial progress message -> we handle progress ourselves!
super(5, null);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
mDataUri = args.getParcelable(ARG_DATA_URI);
mLidRank = args.getInt(ARG_LID_RANK);
mIsSecret = args.getBoolean(ARG_IS_SECRET);
mFingerprint = args.getByteArray(ARG_FINGERPRINT);
mContext = getActivity();
getLoaderManager().initLoader(LOADER_ID_LINKED_ID, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_ID_LINKED_ID:
return new CursorLoader(getActivity(), mDataUri,
UserIdsAdapter.USER_PACKETS_PROJECTION,
Tables.USER_PACKETS + "." + UserPackets.RANK
+ " = " + Integer.toString(mLidRank), null, null);
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
switch (loader.getId()) {
case LOADER_ID_LINKED_ID:
// Nothing to load means break if we are *expected* to load
if (!cursor.moveToFirst()) {
if (mIdLoadedListener != null) {
Notify.create(getActivity(), "Error loading identity!",
Notify.LENGTH_LONG, Style.ERROR).show();
finishFragment();
}
// Or just ignore, this is probably some intermediate state during certify
break;
}
try {
int certStatus = cursor.getInt(UserIdsAdapter.INDEX_VERIFIED);
byte[] data = cursor.getBlob(UserIdsAdapter.INDEX_ATTRIBUTE_DATA);
UriAttribute linkedId = LinkedAttribute.fromAttributeData(data);
loadIdentity(linkedId, certStatus);
if (mIdLoadedListener != null) {
mIdLoadedListener.onIdentityLoaded();
mIdLoadedListener = null;
}
} catch (IOException e) {
Log.e(Constants.TAG, "error parsing identity", e);
Notify.create(getActivity(), "Error parsing identity!",
Notify.LENGTH_LONG, Style.ERROR).show();
finishFragment();
}
break;
}
}
public void finishFragment() {
new Handler().post(new Runnable() {
@Override
public void run() {
FragmentManager manager = getFragmentManager();
manager.removeOnBackStackChangedListener(LinkedIdViewFragment.this);
manager.popBackStack("linked_id", FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
});
}
public interface OnIdentityLoadedListener {
void onIdentityLoaded();
}
public void setOnIdentityLoadedListener(OnIdentityLoadedListener listener) {
mIdLoadedListener = listener;
}
private void loadIdentity(UriAttribute linkedId, int certStatus) {
mLinkedId = linkedId;
if (mLinkedId instanceof LinkedAttribute) {
LinkedResource res = ((LinkedAttribute) mLinkedId).mResource;
mLinkedResource = (LinkedTokenResource) res;
}
if (!mIsSecret) {
switch (certStatus) {
case Certs.VERIFIED_SECRET:
KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
break;
case Certs.VERIFIED_SELF:
KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
break;
default:
KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
break;
}
} else {
mViewHolder.mLinkedIdHolder.vVerified.setImageResource(R.drawable.octo_link_24dp);
}
mViewHolder.mLinkedIdHolder.setData(mContext, mLinkedId);
setShowVerifying(false);
// no resource, nothing further we can do…
if (mLinkedResource == null) {
mViewHolder.vButtonView.setVisibility(View.GONE);
mViewHolder.vButtonVerify.setVisibility(View.GONE);
return;
}
if (mLinkedResource.isViewable()) {
mViewHolder.vButtonView.setVisibility(View.VISIBLE);
mViewHolder.vButtonView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = mLinkedResource.getViewIntent();
if (intent == null) {
return;
}
getActivity().startActivity(intent);
}
});
} else {
mViewHolder.vButtonView.setVisibility(View.GONE);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
static class ViewHolder {
private final View vButtonView;
private final ViewAnimator vVerifyingContainer;
private final ViewAnimator vItemCertified;
private final View vKeySpinnerContainer;
LinkedIdsAdapter.ViewHolder mLinkedIdHolder;
private ViewAnimator vButtonSwitcher;
private CertListWidget vLinkedCerts;
private CertifyKeySpinner vKeySpinner;
private final View vButtonVerify;
private final View vButtonRetry;
private final View vButtonConfirm;
private final ViewAnimator vProgress;
private final TextSwitcher vText;
ViewHolder(View root) {
vLinkedCerts = (CertListWidget) root.findViewById(R.id.linked_id_certs);
vKeySpinner = (CertifyKeySpinner) root.findViewById(R.id.cert_key_spinner);
vKeySpinnerContainer = root.findViewById(R.id.cert_key_spincontainer);
vButtonSwitcher = (ViewAnimator) root.findViewById(R.id.button_animator);
mLinkedIdHolder = new LinkedIdsAdapter.ViewHolder(root);
vButtonVerify = root.findViewById(R.id.button_verify);
vButtonRetry = root.findViewById(R.id.button_retry);
vButtonConfirm = root.findViewById(R.id.button_confirm);
vButtonView = root.findViewById(R.id.button_view);
vVerifyingContainer = (ViewAnimator) root.findViewById(R.id.linked_verify_container);
vItemCertified = (ViewAnimator) root.findViewById(R.id.linked_id_certified);
vProgress = (ViewAnimator) root.findViewById(R.id.linked_cert_progress);
vText = (TextSwitcher) root.findViewById(R.id.linked_cert_text);
}
enum VerifyState {
VERIFYING, VERIFY_OK, VERIFY_ERROR, CERTIFYING
}
void setVerifyingState(Context context, VerifyState state, boolean isSecret) {
switch (state) {
case VERIFYING:
vProgress.setDisplayedChild(0);
vText.setText(context.getString(R.string.linked_text_verifying));
vKeySpinnerContainer.setVisibility(View.GONE);
break;
case VERIFY_OK:
vProgress.setDisplayedChild(1);
if (!isSecret) {
showButton(2);
if (!vKeySpinner.isSingleEntry()) {
vKeySpinnerContainer.setVisibility(View.VISIBLE);
}
} else {
showButton(1);
vKeySpinnerContainer.setVisibility(View.GONE);
}
break;
case VERIFY_ERROR:
showButton(1);
vProgress.setDisplayedChild(2);
vText.setText(context.getString(R.string.linked_text_error));
vKeySpinnerContainer.setVisibility(View.GONE);
break;
case CERTIFYING:
vProgress.setDisplayedChild(0);
vText.setText(context.getString(R.string.linked_text_confirming));
vKeySpinnerContainer.setVisibility(View.GONE);
break;
}
}
void showVerifyingContainer(Context context, boolean show, boolean isSecret) {
if (vVerifyingContainer.getDisplayedChild() == (show ? 1 : 0)) {
return;
}
vVerifyingContainer.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down);
vVerifyingContainer.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down);
vVerifyingContainer.setDisplayedChild(show ? 1 : 0);
vItemCertified.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down);
vItemCertified.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down);
vItemCertified.setDisplayedChild(show || isSecret ? 1 : 0);
}
void showButton(int which) {
if (vButtonSwitcher.getDisplayedChild() == which) {
return;
}
vButtonSwitcher.setDisplayedChild(which);
}
}
private boolean mVerificationState = false;
/** Switches between the 'verifying' ui bit and certificate status. This method
* must behave correctly in all states, showing or hiding the appropriate views
* and cancelling pending operations where necessary.
*
* This method also handles back button functionality in combination with
* onBackStateChanged.
*/
void setShowVerifying(boolean show) {
if (!show) {
if (mInProgress != null) {
mInProgress.cancel(false);
mInProgress = null;
}
getFragmentManager().removeOnBackStackChangedListener(this);
new Handler().post(new Runnable() {
@Override
public void run() {
getFragmentManager().popBackStack("verification",
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
});
if (!mVerificationState) {
return;
}
mVerificationState = false;
mViewHolder.showButton(0);
mViewHolder.vKeySpinnerContainer.setVisibility(View.GONE);
mViewHolder.showVerifyingContainer(mContext, false, mIsSecret);
return;
}
if (mVerificationState) {
return;
}
mVerificationState = true;
FragmentManager manager = getFragmentManager();
manager.beginTransaction().addToBackStack("verification").commit();
manager.executePendingTransactions();
manager.addOnBackStackChangedListener(this);
mViewHolder.showVerifyingContainer(mContext, true, mIsSecret);
}
@Override
public void onBackStackChanged() {
setShowVerifying(false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.linked_id_view_fragment, null);
mViewHolder = new ViewHolder(root);
root.setTag(mViewHolder);
((ImageView) root.findViewById(R.id.status_icon_verified))
.setColorFilter(mContext.getResources().getColor(R.color.android_green_light),
PorterDuff.Mode.SRC_IN);
((ImageView) root.findViewById(R.id.status_icon_invalid))
.setColorFilter(mContext.getResources().getColor(R.color.android_red_light),
PorterDuff.Mode.SRC_IN);
mViewHolder.vButtonVerify.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
verifyResource();
}
});
mViewHolder.vButtonRetry.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
verifyResource();
}
});
mViewHolder.vButtonConfirm.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
initiateCertifying();
}
});
{
Bundle args = new Bundle();
args.putParcelable(CertListWidget.ARG_URI, Certs.buildLinkedIdCertsUri(mDataUri, mLidRank));
args.putBoolean(CertListWidget.ARG_IS_SECRET, mIsSecret);
getLoaderManager().initLoader(CertListWidget.LOADER_ID_LINKED_CERTS,
args, mViewHolder.vLinkedCerts);
}
return root;
}
void verifyResource() {
// only one at a time (no sync needed, mInProgress is only touched in ui thread)
if (mInProgress != null) {
return;
}
setShowVerifying(true);
mViewHolder.vKeySpinnerContainer.setVisibility(View.GONE);
mViewHolder.setVerifyingState(mContext, VerifyState.VERIFYING, mIsSecret);
mInProgress = new AsyncTask<Void,Void,LinkedVerifyResult>() {
@Override
protected LinkedVerifyResult doInBackground(Void... params) {
long timer = System.currentTimeMillis();
LinkedVerifyResult result = mLinkedResource.verify(getActivity(), mFingerprint);
// ux flow: this operation should take at last a second
timer = System.currentTimeMillis() -timer;
if (timer < 1000) try {
Thread.sleep(1000 -timer);
} catch (InterruptedException e) {
// never mind
}
return result;
}
@Override
protected void onPostExecute(LinkedVerifyResult result) {
if (isCancelled()) {
return;
}
if (result.success()) {
mViewHolder.vText.setText(getString(mLinkedResource.getVerifiedText(mIsSecret)));
// hack to preserve bold text
((TextView) mViewHolder.vText.getCurrentView()).setText(
mLinkedResource.getVerifiedText(mIsSecret));
mViewHolder.setVerifyingState(mContext, VerifyState.VERIFY_OK, mIsSecret);
mViewHolder.mLinkedIdHolder.seekAttention();
} else {
mViewHolder.setVerifyingState(mContext, VerifyState.VERIFY_ERROR, mIsSecret);
result.createNotify(getActivity()).show();
}
mInProgress = null;
}
}.execute();
}
private void initiateCertifying() {
if (mIsSecret) {
return;
}
// get the user's passphrase for this key (if required)
mCertifyKeyId = mViewHolder.vKeySpinner.getSelectedKeyId();
if (mCertifyKeyId == key.none || mCertifyKeyId == key.symmetric) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
SubtleAttentionSeeker.tintBackground(mViewHolder.vKeySpinnerContainer, 600).start();
} else {
Notify.create(getActivity(), R.string.select_key_to_certify, Style.ERROR).show();
}
return;
}
mViewHolder.setVerifyingState(mContext, VerifyState.CERTIFYING, false);
cryptoOperation();
}
@Override
public void onCryptoOperationCancelled() {
super.onCryptoOperationCancelled();
// go back to 'verified ok'
setShowVerifying(false);
}
@Nullable
@Override
public Parcelable createOperationInput() {
long masterKeyId = KeyFormattingUtils.convertFingerprintToKeyId(mFingerprint);
CertifyAction action = new CertifyAction(masterKeyId, null,
Collections.singletonList(mLinkedId.toUserAttribute()));
// fill values for this action
CertifyActionsParcel parcel = new CertifyActionsParcel(mCertifyKeyId);
parcel.mCertifyActions.addAll(Collections.singletonList(action));
return parcel;
}
@Override
public void onCryptoOperationSuccess(OperationResult result) {
result.createNotify(getActivity()).show();
// no need to do anything else, we will get a loader refresh!
}
@Override
public void onCryptoOperationError(OperationResult result) {
result.createNotify(getActivity()).show();
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return true;
}
}

Some files were not shown because too many files have changed in this diff Show More