Merge branch 'feature/Encrypt_Temp_Binaries' into feature/Image_Viewer

This commit is contained in:
J-Jamet
2021-02-08 10:58:31 +01:00
19 changed files with 796 additions and 352 deletions

View File

@@ -17,7 +17,7 @@ android {
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}" buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ] manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
@@ -82,6 +82,10 @@ android {
free.res.srcDir 'src/free/res' free.res.srcDir 'src/free/res'
} }
testOptions {
unitTests.includeAndroidResources = true
}
compileOptions { compileOptions {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@@ -120,14 +124,15 @@ dependencies {
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4' implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
// Education // Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
// Apache Commons Collections // Apache Commons
implementation 'commons-collections:commons-collections:3.2.2' implementation 'commons-collections:commons-collections:3.2.2'
// Apache Commons Codec implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
// Icon pack // Icon pack
implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material') implementation project(path: ':icon-pack-material')
// Tests // Tests
androidTestImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@@ -0,0 +1,133 @@
Basic Latin
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
Latin-1 Supplement
¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ­ ® ¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ
Latin Extended-A
Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě Ĝ ĝ Ğ ğ Ġ ġ Ģ ģ Ĥ ĥ Ħ ħ Ĩ ĩ Ī ī Ĭ ĭ Į į İ ı IJ ij Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ ŀ Ł ł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ō ō Ŏ ŏ Ő ő Œ œ Ŕ ŕ Ŗ ŗ Ř ř Ś ś Ŝ ŝ Ş ş Š š Ţ ţ Ť ť Ŧ ŧ Ũ ũ Ū ū Ŭ ŭ Ů ů Ű ű Ų ų Ŵ ŵ Ŷ ŷ Ÿ Ź ź Ż ż Ž ž ſ
Latin Extended-B
ƀ Ɓ Ƃ ƃ Ƅ ƅ Ɔ Ƈ ƈ Ɖ Ɗ Ƌ ƌ ƍ Ǝ Ə Ɛ Ƒ ƒ Ɠ Ɣ ƕ Ɩ Ɨ Ƙ ƙ ƚ ƛ Ɯ Ɲ ƞ Ɵ Ơ ơ Ƣ ƣ Ƥ ƥ Ʀ Ƨ ƨ Ʃ ƪ ƫ Ƭ ƭ Ʈ Ư ư Ʊ Ʋ Ƴ ƴ Ƶ ƶ Ʒ Ƹ ƹ ƺ ƻ Ƽ ƽ ƾ ƿ ǀ ǁ ǂ ǃ DŽ Dž dž LJ Lj lj NJ Nj nj Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ Ǘ ǘ Ǚ ǚ Ǜ ǜ ǝ Ǟ ǟ Ǡ ǡ Ǣ ǣ Ǥ ǥ Ǧ ǧ Ǩ ǩ Ǫ ǫ Ǭ ǭ Ǯ ǯ ǰ DZ Dz dz Ǵ ǵ Ǻ ǻ Ǽ ǽ Ǿ ǿ Ȁ ȁ Ȃ ȃ ...
IPA Extensions
ɐ ɑ ɒ ɓ ɔ ɕ ɖ ɗ ɘ ə ɚ ɛ ɜ ɝ ɞ ɟ ɠ ɡ ɢ ɣ ɤ ɥ ɦ ɧ ɨ ɩ ɪ ɫ ɬ ɭ ɮ ɯ ɰ ɱ ɲ ɳ ɴ ɵ ɶ ɷ ɸ ɹ ɺ ɻ ɼ ɽ ɾ ɿ ʀ ʁ ʂ ʃ ʄ ʅ ʆ ʇ ʈ ʉ ʊ ʋ ʌ ʍ ʎ ʏ ʐ ʑ ʒ ʓ ʔ ʕ ʖ ʗ ʘ ʙ ʚ ʛ ʜ ʝ ʞ ʟ ʠ ʡ ʢ ʣ ʤ ʥ ʦ ʧ ʨ
Spacing Modifier Letters
ʰ ʱ ʲ ʳ ʴ ʵ ʶ ʷ ʸ ʹ ʺ ʻ ʼ ʽ ʾ ʿ ˀ ˁ ˂ ˃ ˄ ˅ ˆ ˇ ˈ ˉ ˊ ˋ ˌ ˍ ˎ ˏ ː ˑ ˒ ˓ ˔ ˕ ˖ ˗ ˘ ˙ ˚ ˛ ˜ ˝ ˞ ˠ ˡ ˢ ˣ ˤ ˥ ˦ ˧ ˨ ˩
Combining Diacritical Marks
̀ ́ ̂ ̃ ̄ ̅ ̆ ̇ ̈ ̉ ̊ ̋ ̌ ̍ ̎ ̏ ̐ ̑ ̒ ̓ ̔ ̕ ̖ ̗ ̘ ̙ ̚ ̛ ̜ ̝ ̞ ̟ ̠ ̡ ̢ ̣ ̤ ̥ ̦ ̧ ̨ ̩ ̪ ̫ ̬ ̭ ̮ ̯ ̰ ̱ ̲ ̳ ̴ ̵ ̶ ̷ ̸ ̹ ̺ ̻ ̼ ̽ ̾ ̿ ̀ ́ ͂ ̓ ̈́ ͅ ͠ ͡
Greek
ʹ ͵ ͺ ; ΄ ΅ Ά · Έ Ή Ί Ό Ύ Ώ ΐ Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή ί ΰ α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ ς σ τ υ φ χ ψ ω ϊ ϋ ό ύ ώ ϐ ϑ ϒ ϓ ϔ ϕ ϖ Ϛ Ϝ Ϟ Ϡ Ϣ ϣ Ϥ ϥ Ϧ ϧ Ϩ ϩ Ϫ ϫ Ϭ ϭ Ϯ ϯ ϰ ϱ ϲ ϳ
Cyrillic
Ё Ђ Ѓ Є Ѕ І Ї Ј Љ Њ Ћ Ќ Ў Џ А Б В Г Д Е Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я ё ђ ѓ є ѕ і ї ј љ њ ћ ќ ў џ Ѡ ѡ Ѣ ѣ Ѥ ѥ Ѧ ѧ Ѩ ѩ Ѫ ѫ Ѭ ѭ Ѯ ѯ Ѱ ѱ Ѳ ѳ Ѵ ѵ Ѷ ѷ Ѹ ѹ Ѻ ѻ Ѽ ѽ Ѿ ѿ Ҁ ҁ ҂ ҃ ...
Armenian
Ա Բ Գ Դ Ե Զ Է Ը Թ Ժ Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ Մ Յ Ն Շ Ո Չ Պ Ջ Ռ Ս Վ Տ Ր Ց Ւ Փ Ք Օ Ֆ ՙ ՚ ՛ ՜ ՝ ՞ ՟ ա բ գ դ ե զ է ը թ ժ ի լ խ ծ կ հ ձ ղ ճ մ յ ն շ ո չ պ ջ ռ ս վ տ ր ց ւ փ ք օ ֆ և ։
Hebrew
֑ ֒ ֓ ֔ ֕ ֖ ֗ ֘ ֙ ֚ ֛ ֜ ֝ ֞ ֟ ֠ ֡ ֣ ֤ ֥ ֦ ֧ ֨ ֩ ֪ ֫ ֬ ֭ ֮ ֯ ְ ֱ ֲ ֳ ִ ֵ ֶ ַ ָ ֹ ֻ ּ ֽ ־ ֿ ׀ ׁ ׂ ׃ ׄ א ב ג ד ה ו ז ח ט י ך כ ל ם מ ן נ ס ע ף פ ץ צ ק ר ש ת װ ױ ײ ׳ ״
Arabic
، ؛ ؟ ء آ أ ؤ إ ئ ا ب ة ت ث ج ح خ د ذ ر ز س ش ص ض ط ظ ع غ ـ ف ق ك ل م ن ه و ى ي ً ٌ ٍ َ ُ ِ ّ ْ ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٪ ٫ ٬ ٭ ٰ ٱ ٲ ٳ ٴ ٵ ٶ ٷ ٸ ٹ ٺ ٻ ټ ٽ پ ٿ ڀ ځ ڂ ڃ ڄ څ چ ڇ ڈ ډ ڊ ڋ ڌ ڍ ڎ ڏ ڐ ڑ ڒ ړ ڔ ڕ ږ ڗ ژ ڙ ښ ڛ ڜ ڝ ڞ ڟ ڠ ڡ ڢ ڣ ڤ ڥ ڦ ڧ ڨ ک ڪ ګ ڬ ڭ ڮ گ ڰ ڱ ...
Devanagari
ँ ं अ आ इ ई उ ऊ ऋ ऌ ऍ ऎ ए ऐ ऑ ऒ ओ औ क ख ग घ ङ च छ ज झ ञ ट ठ ड ढ ण त थ द ध न ऩ प फ ब भ म य र ऱ ल ळ ऴ व श ष स ह ़ ऽ ा ि ी ु ू ृ ॄ ॅ ॆ े ै ॉ ॊ ो ौ ् ॐ ॑ ॒ ॓ ॔ क़ ख़ ग़ ज़ ड़ ढ़ फ़ य़ ॠ ॡ ॢ ॣ । ॥ १ २ ३ ४ ५ ६ ७ ८ ९ ॰
Bengali
ঁ ং ঃ অ আ ই ঈ উ ঊ ঋ ঌ এ ঐ ও ঔ ক খ গ ঘ ঙ চ ছ জ ঝ ঞ ট ঠ ড ঢ ণ ত থ দ ধ ন প ফ ব ভ ম য র ল শ ষ স হ ় া ি ী ু ূ ৃ ৄ ে ৈ ো ৌ ্ ৗ ড় ঢ় য় ৠ ৡ ৢ ৣ ১ ২ ৩ ৫ ৬ ৮ ৯ ৰ ৱ ৲ ৳ ৴ ৵ ৶ ৷ ৸ ৹ ৺
Gurmukhi
ਂ ਅ ਆ ਇ ਈ ਉ ਊ ਏ ਐ ਓ ਔ ਕ ਖ ਗ ਘ ਙ ਚ ਛ ਜ ਝ ਞ ਟ ਠ ਡ ਢ ਣ ਤ ਥ ਦ ਧ ਨ ਪ ਫ ਬ ਭ ਮ ਯ ਰ ਲ ਲ਼ ਵ ਸ਼ ਸ ਹ ਼ ਾ ਿ ੀ ੁ ੂ ੇ ੈ ੋ ੌ ੍ ਖ਼ ਗ਼ ਜ਼ ੜ ਫ਼ ੨ ੩ ੫ ੬ ੭ ੮ ੯ ੰ ੱ ੲ ੳ ੴ
Gujarati
ઁ ં અ આ ઇ ઈ ઉ ઊ ઋ ઍ એ ઐ ઑ ઓ ઔ ક ખ ગ ઘ ઙ ચ છ જ ઝ ઞ ટ ઠ ડ ઢ ણ ત થ દ ધ ન પ ફ બ ભ મ ય ર લ ળ વ શ ષ સ હ ઼ ઽ ા િ ી ુ ૂ ૃ ૄ ૅ ે ૈ ૉ ો ૌ ્ ૐ ૠ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯
Oriya
ଁ ଂ ଅ ଆ ଇ ଈ ଉ ଊ ଋ ଌ ଏ ଐ ଓ ଔ କ ଖ ଗ ଘ ଙ ଚ ଛ ଜ ଝ ଞ ଟ ଡ ଢ ଣ ତ ଥ ଦ ଧ ନ ପ ଫ ବ ଭ ମ ଯ ର ଲ ଳ ଶ ଷ ସ ହ ଼ ଽ ା ି ୀ ୁ ୂ ୃ େ ୈ ୋ ୌ ୍ ୖ ୗ ଡ଼ ଢ଼ ୟ ୠ ୡ ୩ ୪ ୫ ୬ ୭ ୮ ୯ ୰
Tamil
ஂ ஃ அ ஆ இ ஈ உ ஊ எ ஏ ஐ ஒ ஓ ஔ க ங ச ஜ ஞ ட ண த ந ன ப ம ய ர ற ல ள ழ வ ஷ ஸ ஹ ா ி ீ ு ூ ெ ே ை ொ ோ ௌ ் ௗ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯ ௰ ௱ ௲
Telugu
ః అ ఆ ఇ ఈ ఉ ఊ ఋ ఌ ఎ ఏ ఐ ఒ ఓ ఔ క ఖ గ ఘ ఙ చ ఛ జ ఝ ఞ ట ఠ డ ఢ ణ త థ ద ధ న ప ఫ బ భ మ య ర ఱ ల ళ వ శ ష స హ ా ి ీ ు ూ ృ ౄ ె ే ై ొ ో ౌ ్ ౕ ౖ ౠ ౡ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯
Kannada
ಃ ಅ ಆ ಇ ಈ ಉ ಊ ಋ ಌ ಎ ಏ ಐ ಒ ಓ ಔ ಕ ಖ ಗ ಘ ಙ ಚ ಛ ಜ ಝ ಞ ಟ ಠ ಡ ಢ ಣ ತ ಥ ದ ಧ ನ ಪ ಫ ಬ ಭ ಮ ಯ ರ ಱ ಲ ಳ ವ ಶ ಷ ಸ ಹ ಾ ಿ ೀ ು ೂ ೃ ೄ ೆ ೇ ೈ ೊ ೋ ೌ ್ ೕ ೖ ೞ ೠ ೡ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯
Malayalam
ഃ അ ആ ഇ ഈ ഉ ഊ ഋ ഌ എ ഏ ഐ ഒ ഓ ഔ ക ഖ ഗ ഘ ങ ച ഛ ജ ഝ ഞ ട ഡ ഢ ണ ത ഥ ദ ധ ന പ ഫ ബ ഭ മ യ ര റ ല ള ഴ വ ശ ഷ സ ഹ ാ ി ീ ു ൂ ൃ െ േ ൈ ൊ ോ ൌ ് ൗ ൠ ൡ ൧ ൨ ൩ ൪ ൫ ൬ ൮ ൯
Thai
ก ข ฃ ค ฅ ฆ ง จ ฉ ช ซ ฌ ญ ฎ ฏ ฐ ฑ ฒ ณ ด ต ถ ท ธ น บ ป ผ ฝ พ ฟ ภ ม ย ร ฤ ล ฦ ว ศ ษ ส ห ฬ อ ฮ ฯ ะ ั า ำ ิ ี ึ ื ุ ู ฺ ฿ เ แ โ ใ ไ ๅ ๆ ็ ่ ้ ๊ ๋ ์ ํ ๎ ๏ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙ ๚ ๛
Lao
ກ ຂ ຄ ງ ຈ ຊ ຍ ດ ຕ ຖ ທ ນ ບ ປ ຜ ຝ ພ ຟ ມ ຢ ຣ ລ ວ ສ ຫ ອ ຮ ຯ ະ ັ າ ຳ ິ ີ ຶ ື ຸ ູ ົ ຼ ຽ ເ ແ ໂ ໃ ໄ ໆ ່ ້ ໊ ໋ ໌ ໍ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙ ໜ ໝ
Tibetan
ༀ ༁ ༂ ༃ ༄ ༅ ༆ ༇ ༈ ༉ ༊ ་ ༌ ། ༎ ༏ ༐ ༑ ༒ ༓ ༔ ༕ ༖ ༗ ༘ ༙ ༚ ༛ ༜ ༝ ༞ ༟ ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩ ༪ ༫ ༬ ༭ ༮ ༯ ༰ ༱ ༲ ༳ ༴ ༵ ༶ ༷ ༸ ༹ ༺ ༻ ༼ ༽ ༾ ༿ ཀ ཁ ག གྷ ང ཅ ཆ ཇ ཉ ཊ ཋ ཌ ཌྷ ཎ ཏ ཐ ད དྷ ན པ ཕ བ བྷ མ ཙ ཚ ཛ ཛྷ ཝ ཞ ཟ འ ཡ ར ལ ཤ ཥ ས ཧ ཨ ཀྵ ཱ ི ཱི ུ ཱུ ྲྀ ཷ ླྀ ཹ ེ ཻ ོ ཽ ཾ ཿ ྀ ཱྀ ྂ ྃ ྄ ྅ ྆ ྇ ...
Georgian
Ⴀ Ⴁ Ⴂ Ⴃ Ⴄ Ⴅ Ⴆ Ⴇ Ⴈ Ⴉ Ⴊ Ⴋ Ⴌ Ⴍ Ⴎ Ⴏ Ⴐ Ⴑ Ⴒ Ⴓ Ⴔ Ⴕ Ⴖ Ⴗ Ⴘ Ⴙ Ⴚ Ⴛ Ⴜ Ⴝ Ⴞ Ⴟ Ⴠ Ⴡ Ⴢ Ⴣ Ⴤ Ⴥ ა ბ გ დ ე ვ ზ თ ი კ ლ მ ნ ო პ ჟ რ ს ტ უ ფ ქ ღ შ ჩ ც ძ წ ჭ ხ ჯ ჰ ჱ ჲ ჳ ჴ ჵ ჶ ჻
Hangul Jamo
ᄀ ᄁ ᄂ ᄃ ᄄ ᄅ ᄆ ᄇ ᄈ ᄉ ᄊ ᄋ ᄌ ᄍ ᄎ ᄏ ᄐ ᄑ ᄒ ᄓ ᄔ ᄕ ᄖ ᄗ ᄘ ᄙ ᄚ ᄛ ᄜ ᄝ ᄞ ᄟ ᄠ ᄡ ᄢ ᄣ ᄤ ᄥ ᄦ ᄧ ᄨ ᄩ ᄪ ᄫ ᄬ ᄭ ᄮ ᄯ ᄰ ᄱ ᄲ ᄳ ᄴ ᄵ ᄶ ᄷ ᄸ ᄹ ᄺ ᄻ ᄼ ᄽ ᄾ ᄿ ᅀ ᅁ ᅂ ᅃ ᅄ ᅅ ᅆ ᅇ ᅈ ᅉ ᅊ ᅋ ᅌ ᅍ ᅎ ᅏ ᅐ ᅑ ᅒ ᅓ ᅔ ᅕ ᅖ ᅗ ᅘ ᅙ ᅡ ᅢ ᅣ ᅤ ᅥ ᅦ ᅧ ᅨ ᅩ ᅪ ᅫ ᅬ ᅭ ᅮ ᅯ ᅰ ᅱ ᅲ ᅳ ᅴ ᅵ ᅶ ᅷ ᅸ ᅹ ᅺ ᅻ ᅼ ᅽ ᅾ ᅿ ᆀ ᆁ ᆂ ᆃ ᆄ ...
Latin Extended Additional
Ḁ ḁ Ḃ ḃ Ḅ ḅ Ḇ ḇ Ḉ ḉ Ḋ ḋ Ḍ ḍ Ḏ ḏ Ḑ ḑ Ḓ ḓ Ḕ ḕ Ḗ ḗ Ḙ ḙ Ḛ ḛ Ḝ ḝ Ḟ ḟ Ḡ ḡ Ḣ ḣ Ḥ ḥ Ḧ ḧ Ḩ ḩ Ḫ ḫ Ḭ ḭ Ḯ ḯ Ḱ ḱ Ḳ ḳ Ḵ ḵ Ḷ ḷ Ḹ ḹ Ḻ ḻ Ḽ ḽ Ḿ ḿ Ṁ ṁ Ṃ ṃ Ṅ ṅ Ṇ ṇ Ṉ ṉ Ṋ ṋ Ṍ ṍ Ṏ ṏ Ṑ ṑ Ṓ ṓ Ṕ ṕ Ṗ ṗ Ṙ ṙ Ṛ ṛ Ṝ ṝ Ṟ ṟ Ṡ ṡ Ṣ ṣ Ṥ ṥ Ṧ ṧ Ṩ ṩ Ṫ ṫ Ṭ ṭ Ṯ ṯ Ṱ ṱ Ṳ ṳ Ṵ ṵ Ṷ ṷ Ṹ ṹ Ṻ ṻ Ṽ ṽ Ṿ ṿ ...
Greek Extended
ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ ἐ ἑ ἒ ἓ ἔ ἕ Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ ὀ ὁ ὂ ὃ ὄ ὅ Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ Ὑ Ὓ Ὕ Ὗ ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ...
General Punctuation
  — ― ‖ ‗ “ ” „ ‟ † ‡ • ‣ ‥ … ‧ ‰ ‱ ″ ‴ ‶ ‷ ‸ ※ ‼ ‽ ‾ ‿ ⁀ ⁅ ⁆
Superscripts and Subscripts
⁰ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎
Currency Symbols
₠ ₡ ₢ ₣ ₤ ₥ ₦ ₧ ₨ ₩ ₪ ₫
Combining Marks for Symbols
⃐ ⃑ ⃒ ⃓ ⃔ ⃕ ⃖ ⃗ ⃘ ⃙ ⃚ ⃛ ⃜ ⃝ ⃞ ⃟ ⃠ ⃡
Letterlike Symbols
℀ ℁ ℃ ℄ ℅ ℆ ℇ ℈ ℉ № ℗ ℘ ℞ ℟ ℠ ℡ ™ ℣ ℥ Ω ℧ ℵ ℶ ℷ ℸ
Number Forms
⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞ ⅟ Ⅱ Ⅲ Ⅳ Ⅵ Ⅶ Ⅷ Ⅸ Ⅺ Ⅻ ⅱ ⅲ ⅳ ⅵ ⅶ ⅷ ⅸ ⅺ ⅻ ⅿ ↀ ↁ ↂ
Arrows
← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ↚ ↛ ↜ ↝ ↞ ↟ ↠ ↡ ↢ ↣ ↤ ↥ ↦ ↧ ↨ ↩ ↪ ↫ ↬ ↭ ↮ ↯ ↰ ↱ ↲ ↳ ↴ ↵ ↶ ↷ ↸ ↹ ↺ ↻ ↼ ↽ ↾ ↿ ⇀ ⇁ ⇂ ⇃ ⇄ ⇅ ⇆ ⇇ ⇈ ⇉ ⇊ ⇋ ⇌ ⇍ ⇎ ⇏ ⇐ ⇑ ⇒ ⇓ ⇔ ⇕ ⇖ ⇗ ⇘ ⇙ ⇚ ⇛ ⇜ ⇝ ⇞ ⇟ ⇠ ⇡ ⇢ ⇣ ⇤ ⇥ ⇦ ⇧ ⇨ ⇩ ⇪
Mathematical Operators
∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏ ∐ ∑ ∓ ∔ ∘ ∙ √ ∛ ∜ ∝ ∞ ∟ ∠ ∡ ∢ ∤ ∥ ∦ ∧ ∫ ∬ ∭ ∮ ∯ ∰ ∱ ∲ ∳ ∴ ∵ ∷ ∸ ∹ ∺ ∻ ∽ ∾ ∿ ≀ ≁ ≂ ≃ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≏ ≐ ≑ ≒ ≓ ≔ ≕ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≠ ≡ ≢ ≣ ≤ ≥ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ...
Miscellaneous Technical
⌀ ⌂ ⌃ ⌄ ⌅ ⌆ ⌇ ⌈ ⌉ ⌊ ⌋ ⌌ ⌍ ⌎ ⌏ ⌐ ⌑ ⌒ ⌓ ⌔ ⌕ ⌖ ⌗ ⌘ ⌙ ⌚ ⌛ ⌜ ⌝ ⌞ ⌟ ⌠ ⌡ ⌢ ⌣ ⌤ ⌥ ⌦ ⌧ ⌨ 〈 〉 ⌫ ⌬ ⌭ ⌮ ⌯ ⌰ ⌱ ⌲ ⌳ ⌴ ⌵ ⌶ ⌷ ⌸ ⌹ ⌺ ⌻ ⌼ ⌽ ⌾ ⌿ ⍀ ⍁ ⍂ ⍃ ⍄ ⍅ ⍆ ⍇ ⍈ ⍉ ⍊ ⍋ ⍌ ⍍ ⍎ ⍏ ⍐ ⍑ ⍒ ⍓ ⍔ ⍕ ⍖ ⍗ ⍘ ⍙ ⍚ ⍛ ⍜ ⍝ ⍞ ⍟ ⍠ ⍡ ⍢ ⍣ ⍤ ⍥ ⍦ ⍧ ⍨ ⍩ ⍪ ⍫ ⍬ ⍭ ⍮ ⍯ ⍰ ⍱ ⍲ ⍵ ⍶ ⍷ ⍸ ⍹
Control Pictures
␀ ␁ ␂ ␃ ␄ ␅ ␆ ␇ ␈ ␉ ␊ ␋ ␌ ␍ ␎ ␏ ␐ ␑ ␒ ␓ ␔ ␕ ␖ ␗ ␘ ␙ ␚ ␛ ␜ ␝ ␞ ␟ ␠ ␡ ␢ ␣ ␤
Optical Character Recognition
⑀ ⑁ ⑂ ⑃ ⑄ ⑅ ⑆ ⑇ ⑈ ⑉ ⑊
Enclosed Alphanumerics
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ...
Box Drawing
─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
Block Elements
▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕
Geometric Shapes
■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯
Miscellaneous Symbols
☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ ☊ ☋ ☌ ☍ ☎ ☏ ☐ ☑ ☒ ☓ ☚ ☛ ☜ ☝ ☞ ☟ ☠ ☡ ☢ ☣ ☤ ☥ ☦ ☧ ☨ ☩ ☪ ☫ ☬ ☭ ☮ ☯ ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ ☸ ☹ ☺ ☻ ☼ ☽ ☾ ☿ ♀ ♁ ♂ ♃ ♄ ♅ ♆ ♇ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯
Dingbats
✁ ✂ ✃ ✄ ✆ ✇ ✈ ✉ ✌ ✍ ✎ ✏ ✐ ✑ ✒ ✓ ✔ ✕ ✖ ✗ ✘ ✙ ✚ ✛ ✜ ✝ ✞ ✟ ✠ ✡ ✢ ✣ ✤ ✥ ✦ ✧ ✩ ✪ ✫ ✬ ✭ ✮ ✯ ✰ ✱ ✲ ✳ ✴ ✵ ✶ ✷ ✸ ✹ ✺ ✻ ✼ ✽ ✾ ✿ ❀ ❁ ❂ ❃ ❄ ❅ ❆ ❇ ❈ ❉ ❊ ❋ ❍ ❏ ❐ ❑ ❒ ❖ ❘ ❙ ❚ ❛ ❜ ❝ ❞ ❡ ❢ ❣ ❤ ❥ ❦ ❧ ❶ ❷ ❸ ❹ ❺ ❻ ❼ ❽ ❾ ❿ ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉ ➊ ➋ ➌ ➍ ➎ ➏ ➐ ➑ ➒ ➓ ➔ ➘ ➙ ➚ ➛ ➜ ➝ ...
CJK Symbols and Punctuation
  、 。 〃 〄 々 〆 〈 〉 《 》 「 」 『 』 【 】 〒 〓 〖 〗 〘 〙 〚 〛 〜 〝 〞 〟 〠 〡 〢 〣 〤 〥 〦 〧 〨 〩 〪 〫 〬 〭 〮 〯 〰 〱 〲 〴 〵 〶 〷 〿
Hiragana
ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん ゔ ゙ ゚ ゛ ゜ ゝ ゞ
Katakana
ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ ゲ コ ゴ サ ザ シ ジ ス ズ セ ゼ ソ ゾ タ ダ チ ヂ ッ ツ ヅ テ デ ト ド ナ ニ ヌ ネ ハ バ パ ヒ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ム メ モ ャ ヤ ュ ユ ョ ヨ ラ リ ル レ ロ ヮ ワ ヰ ヱ ヲ ン ヴ ヵ ヶ ヷ ヸ ヹ ヺ ・ ー ヽ ヾ
Bopomofo
ㄅ ㄆ ㄇ ㄈ ㄉ ㄊ ㄋ ㄌ ㄍ ㄎ ㄏ ㄐ ㄑ ㄒ ㄓ ㄔ ㄕ ㄖ ㄗ ㄘ ㄙ ㄚ ㄛ ㄜ ㄝ ㄞ ㄟ ㄠ ㄡ ㄢ ㄣ ㄤ ㄥ ㄦ ㄧ ㄨ ㄩ ㄪ ㄫ ㄬ
Hangul Compatibility Jamo
ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄸ ㄹ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅃ ㅄ ㅅ ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ ㅥ ㅦ ㅧ ㅨ ㅩ ㅪ ㅫ ㅬ ㅭ ㅮ ㅯ ㅰ ㅱ ㅲ ㅳ ㅴ ㅵ ㅶ ㅷ ㅸ ㅹ ㅺ ㅻ ㅼ ㅽ ㅾ ㅿ ㆀ ㆁ ㆂ ㆃ ㆄ ㆅ ㆆ ㆇ ㆈ ㆉ ㆊ ㆋ ㆌ ㆍ ㆎ
Kanbun
㆐ ㆑ ㆒ ㆓ ㆔ ㆕ ㆖ ㆗ ㆘ ㆙ ㆚ ㆛ ㆜ ㆝ ㆞ ㆟
Enclosed CJK Letters and Months
㈀ ㈁ ㈂ ㈃ ㈄ ㈅ ㈆ ㈇ ㈈ ㈉ ㈊ ㈋ ㈌ ㈍ ㈎ ㈏ ㈐ ㈑ ㈒ ㈓ ㈔ ㈕ ㈖ ㈗ ㈘ ㈙ ㈚ ㈛ ㈜ ㈠ ㈡ ㈢ ㈣ ㈤ ㈥ ㈦ ㈧ ㈨ ㈩ ㈪ ㈫ ㈬ ㈭ ㈮ ㈯ ㈰ ㈱ ㈲ ㈳ ㈴ ㈵ ㈶ ㈷ ㈸ ㈹ ㈺ ㈻ ㈼ ㈽ ㈾ ㈿ ㉀ ㉁ ㉂ ㉃ ㉠ ㉡ ㉢ ㉣ ㉤ ㉥ ㉦ ㉧ ㉨ ㉩ ㉪ ㉫ ㉬ ㉭ ㉮ ㉯ ㉰ ㉱ ㉲ ㉳ ㉴ ㉵ ㉶ ㉷ ㉸ ㉹ ㉺ ㉻ ㉿ ㊀ ㊁ ㊂ ㊃ ㊄ ㊅ ㊆ ㊇ ㊈ ㊉ ㊊ ㊋ ㊌ ㊍ ㊎ ㊏ ㊐ ㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ ㊝ ㊞ ㊟ ㊠ ㊡ ...
CJK Compatibility
㌀ ㌁ ㌂ ㌃ ㌄ ㌅ ㌆ ㌇ ㌈ ㌉ ㌊ ㌋ ㌌ ㌍ ㌎ ㌏ ㌐ ㌑ ㌒ ㌓ ㌔ ㌕ ㌖ ㌗ ㌘ ㌙ ㌚ ㌛ ㌜ ㌝ ㌞ ㌟ ㌠ ㌡ ㌢ ㌣ ㌤ ㌥ ㌦ ㌧ ㌨ ㌩ ㌪ ㌫ ㌬ ㌭ ㌮ ㌯ ㌰ ㌱ ㌲ ㌳ ㌴ ㌵ ㌶ ㌷ ㌸ ㌹ ㌺ ㌻ ㌼ ㌽ ㌾ ㌿ ㍀ ㍁ ㍂ ㍃ ㍄ ㍅ ㍆ ㍇ ㍈ ㍉ ㍊ ㍋ ㍌ ㍍ ㍎ ㍏ ㍐ ㍑ ㍒ ㍓ ㍔ ㍕ ㍖ ㍗ ㍘ ㍙ ㍚ ㍛ ㍜ ㍝ ㍞ ㍟ ㍠ ㍡ ㍢ ㍣ ㍤ ㍥ ㍦ ㍧ ㍨ ㍩ ㍪ ㍫ ㍬ ㍭ ㍮ ㍯ ㍰ ㍱ ㍲ ㍳ ㍴ ㍵ ㍶ ㍻ ㍼ ㍽ ㍾ ㍿ ㎀ ㎁ ㎂ ㎃ ...
CJK Unified Ideographs
一 丁 丂 七 丄 丅 丆 万 丈 三 上 下 丌 不 与 丏 丐 丑 丒 专 且 丕 世 丗 丘 丙 业 丛 东 丝 丞 丟 丠 両 丢 丣 两 严 並 丧 丨 丩 个 丫 丬 中 丮 丯 丰 丱 串 丳 临 丵 丷 丸 丹 为 主 丼 丽 举 丿 乀 乁 乂 乃 乄 久 乆 乇 么 义 乊 之 乌 乍 乎 乏 乐 乑 乒 乓 乔 乕 乖 乗 乘 乙 乚 乛 乜 九 乞 也 习 乡 乢 乣 乤 乥 书 乧 乨 乩 乪 乫 乬 乭 乮 乯 买 乱 乲 乳 乴 乵 乶 乷 乸 乹 乺 乻 乼 乽 乾 乿 ...
Hangul Syllables
가 각 갂 갃 간 갅 갆 갇 갈 갉 갊 갋 갌 갍 갎 갏 감 갑 값 갓 갔 강 갖 갗 갘 같 갚 갛 개 객 갞 갟 갠 갡 갢 갣 갤 갥 갦 갧 갨 갩 갪 갫 갬 갭 갮 갯 갰 갱 갲 갳 갴 갵 갶 갷 갸 갹 갺 갻 갼 갽 갾 갿 걀 걁 걂 걃 걄 걅 걆 걇 걈 걉 걊 걋 걌 걍 걎 걏 걐 걑 걒 걓 걔 걕 걖 걗 걘 걙 걚 걛 걜 걝 걞 걟 걠 걡 걢 걣 걤 걥 걦 걧 걨 걩 걪 걫 걬 걭 걮 걯 거 걱 걲 걳 건 걵 걶 걷 걸 걹 걺 걻 걼 걽 걾 걿 ...
Private Use
                                                                                                                                ...
CJK Compatibility Ideographs
豈 更 車 賈 滑 串 句 龜 龜 契 金 喇 奈 懶 癩 羅 蘿 螺 裸 邏 樂 洛 烙 珞 落 酪 駱 亂 卵 欄 爛 蘭 鸞 嵐 濫 藍 襤 拉 臘 蠟 廊 朗 浪 狼 郎 來 冷 勞 擄 櫓 爐 盧 老 蘆 虜 路 露 魯 鷺 碌 祿 綠 菉 錄 鹿 論 壟 弄 籠 聾 牢 磊 賂 雷 壘 屢 樓 淚 漏 累 縷 陋 勒 肋 凜 凌 稜 綾 菱 陵 讀 拏 樂 諾 丹 寧 怒 率 異 北 磻 便 復 不 泌 數 索 參 塞 省 葉 說 殺 辰 沈 拾 若 掠 略 亮 兩 凉 梁 糧 良 諒 量 勵 ...
Alphabetic Presentation Forms
ff fi fl ffi ffl ſt st ﬓ ﬔ ﬕ ﬖ ﬗ ﬞ ײַ ﬠ ﬡ ﬢ ﬣ ﬤ ﬥ ﬦ ﬧ ﬨ ﬩ שׁ שׂ שּׁ שּׂ אַ אָ אּ בּ גּ דּ הּ וּ זּ טּ יּ ךּ כּ לּ מּ נּ סּ ףּ פּ צּ קּ רּ שּ תּ וֹ בֿ כֿ פֿ ﭏ
Arabic Presentation Forms-A
ﭐ ﭑ ﭒ ﭓ ﭔ ﭕ ﭖ ﭗ ﭘ ﭙ ﭚ ﭛ ﭜ ﭝ ﭞ ﭟ ﭠ ﭡ ﭢ ﭣ ﭤ ﭥ ﭦ ﭧ ﭨ ﭩ ﭪ ﭫ ﭬ ﭭ ﭮ ﭯ ﭰ ﭱ ﭲ ﭳ ﭴ ﭵ ﭶ ﭷ ﭸ ﭹ ﭺ ﭻ ﭼ ﭽ ﭾ ﭿ ﮀ ﮁ ﮂ ﮃ ﮄ ﮅ ﮆ ﮇ ﮈ ﮉ ﮊ ﮋ ﮌ ﮍ ﮎ ﮏ ﮐ ﮑ ﮒ ﮓ ﮔ ﮕ ﮖ ﮗ ﮘ ﮙ ﮚ ﮛ ﮜ ﮝ ﮞ ﮟ ﮠ ﮡ ﮢ ﮣ ﮤ ﮥ ﮮ ﮯ ﮰ ﮱ ﯓ ﯔ ﯕ ﯖ ﯗ ﯘ ﯙ ﯚ ﯛ ﯜ ﯝ ﯞ ﯟ ﯠ ﯡ ﯢ ﯣ ﯤ ﯥ ﯦ ﯧ ﯨ ﯩ ﯪ ﯫ ﯬ ﯭ ﯮ ﯯ ﯰ ...
Combining Half Marks
︠ ︡ ︢ ︣
CJK Compatibility Forms
︱ ︲ ︳ ︴ ︵ ︶ ︷ ︸ ︹ ︺ ︻ ︼ ︽ ︾ ︿ ﹀ ﹁ ﹂ ﹃ ﹄ ﹉ ﹊ ﹋ ﹌
Small Form Variants
﹐ ﹑ ﹒ ﹔ ﹕ ﹖ ﹗ ﹙ ﹚ ﹛ ﹜ ﹝ ﹞ ﹟ ﹠ ﹡ ﹢ ﹣ ﹤ ﹥ ﹦ ﹩ ﹪ ﹫
Arabic Presentation Forms-B
ﹰ ﹱ ﹲ ﹴ ﹶ ﹷ ﹸ ﹹ ﹺ ﹻ ﹼ ﹽ ﹾ ﹿ ﺀ ﺁ ﺂ ﺃ ﺄ ﺅ ﺆ ﺇ ﺈ ﺉ ﺊ ﺋ ﺌ ﺏ ﺐ ﺑ ﺒ ﺓ ﺔ ﺕ ﺖ ﺗ ﺘ ﺙ ﺚ ﺛ ﺜ ﺝ ﺞ ﺟ ﺠ ﺡ ﺢ ﺣ ﺤ ﺥ ﺦ ﺧ ﺨ ﺩ ﺪ ﺫ ﺬ ﺭ ﺮ ﺯ ﺰ ﺱ ﺲ ﺳ ﺴ ﺵ ﺶ ﺷ ﺸ ﺹ ﺺ ﺻ ﺼ ﺽ ﺾ ﺿ ﻀ ﻁ ﻂ ﻃ ﻄ ﻅ ﻆ ﻇ ﻈ ﻉ ﻊ ﻋ ﻌ ﻍ ﻎ ﻏ ﻐ ﻑ ﻒ ﻓ ﻔ ﻕ ﻖ ﻗ ﻘ ﻙ ﻚ ﻛ ﻜ ﻝ ﻞ ﻟ ﻠ ﻡ ﻢ ﻣ ﻤ ﻥ ﻦ ﻧ ﻨ ﻭ ﻮ ﻯ ﻰ ﻱ ...
Halfwidth and Fullwidth Forms
_ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ ...
Specials

Specials
<20>

View File

@@ -0,0 +1,156 @@
package com.kunzisoft.keepass.tests.stream
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.utils.UriUtil
import junit.framework.TestCase.assertEquals
import org.junit.Test
import java.io.DataInputStream
import java.io.File
import java.io.InputStream
import java.lang.Exception
import java.security.MessageDigest
class BinaryAttachmentTest {
private val context: Context by lazy {
InstrumentationRegistry.getInstrumentation().context
}
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
private val loadedKey = Database.LoadedKey.generateNewCipherKey()
private fun saveBinary(asset: String, binaryAttachment: BinaryAttachment) {
context.assets.open(asset).use { assetInputStream ->
binaryAttachment.getOutputDataStream(loadedKey).use { binaryOutputStream ->
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
binaryOutputStream.write(buffer)
}
}
}
}
@Test
fun testSaveTextInCache() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
assertEquals("Save text binary length failed.", binaryA.length, binaryB.length)
assertEquals("Save text binary MD5 failed.", binaryA.md5(), binaryB.md5())
}
@Test
fun testSaveImageInCache() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
assertEquals("Save image binary length failed.", binaryA.length, binaryB.length)
assertEquals("Save image binary failed.", binaryA.md5(), binaryB.md5())
}
@Test
fun testCompressText() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
val binaryC = BinaryAttachment(fileC)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
saveBinary(TEST_TEXT_ASSET, binaryC)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress text length failed.", binaryA.length, binaryB.length)
assertEquals("Compress text MD5 failed.", binaryA.md5(), binaryB.md5())
binaryB.decompress(loadedKey)
assertEquals("Decompress text length failed.", binaryB.length, binaryC.length)
assertEquals("Decompress text MD5 failed.", binaryB.md5(), binaryC.md5())
}
@Test
fun testCompressImage() {
val binaryA = BinaryAttachment(fileA)
var binaryB = BinaryAttachment(fileB)
val binaryC = BinaryAttachment(fileC)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
saveBinary(TEST_IMAGE_ASSET, binaryC)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress image length failed.", binaryA.length, binaryA.length)
assertEquals("Compress image failed.", binaryA.md5(), binaryA.md5())
binaryB = BinaryAttachment(fileB, true)
binaryB.decompress(loadedKey)
assertEquals("Decompress image length failed.", binaryB.length, binaryC.length)
assertEquals("Decompress image failed.", binaryB.md5(), binaryC.md5())
}
@Test
fun testReadText() {
val binaryA = BinaryAttachment(fileA)
saveBinary(TEST_TEXT_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
@Test
fun testReadImage() {
val binaryA = BinaryAttachment(fileA)
saveBinary(TEST_IMAGE_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
private fun streamAreEquals(inputStreamA: InputStream,
inputStreamB: InputStream): Boolean {
val bufferA = ByteArray(DEFAULT_BUFFER_SIZE)
val bufferB = ByteArray(DEFAULT_BUFFER_SIZE)
val dataInputStreamB = DataInputStream(inputStreamB)
try {
var len: Int
while (inputStreamA.read(bufferA).also { len = it } > 0) {
dataInputStreamB.readFully(bufferB, 0, len)
for (i in 0 until len) {
if (bufferA[i] != bufferB[i])
return false
}
}
return inputStreamB.read() < 0 // is the end of the second file also.
} catch (e: Exception) {
return false
}
finally {
inputStreamA.close()
inputStreamB.close()
}
}
private fun BinaryAttachment.md5(): String {
val md = MessageDigest.getInstance("MD5")
return this.getInputDataStream(loadedKey).use { fis ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
generateSequence {
when (val bytesRead = fis.read(buffer)) {
-1 -> null
else -> bytesRead
}
}.forEach { bytesRead -> md.update(buffer, 0, bytesRead) }
md.digest().joinToString("") { "%02x".format(it) }
}
}
companion object {
private const val TEST_FILE_CACHE_A = "testA"
private const val TEST_FILE_CACHE_B = "testB"
private const val TEST_FILE_CACHE_C = "testC"
private const val TEST_IMAGE_ASSET = "test_image.png"
private const val TEST_TEXT_ASSET = "test_text.txt"
}
}

View File

@@ -90,7 +90,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
holder.binaryFileTitle.setTextColor(mTitleColor) holder.binaryFileTitle.setTextColor(mTitleColor)
} }
holder.binaryFileSize.text = Formatter.formatFileSize(context, holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachmentState.attachment.binaryAttachment.length()) entryAttachmentState.attachment.binaryAttachment.length)
holder.binaryFileCompression.apply { holder.binaryFileCompression.apply {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) { if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources) text = CompressionAlgorithm.GZip.getName(context.resources)

View File

@@ -47,7 +47,11 @@ import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import java.io.* import java.io.*
import java.security.Key
import java.security.SecureRandom
import java.util.* import java.util.*
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -75,6 +79,19 @@ class Database {
var loadTimestamp: Long? = null var loadTimestamp: Long? = null
private set private set
/**
* Cipher key regenerated when the database is loaded and closed
* Can be used to temporarily store database elements
*/
var loadedCipherKey: LoadedKey?
private set(value) {
mDatabaseKDB?.loadedCipherKey = value
mDatabaseKDBX?.loadedCipherKey = value
}
get() {
return mDatabaseKDB?.loadedCipherKey ?: mDatabaseKDBX?.loadedCipherKey
}
val iconFactory: IconImageFactory val iconFactory: IconImageFactory
get() { get() {
return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory() return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
@@ -321,12 +338,26 @@ class Database {
} }
fun createData(databaseUri: Uri, databaseName: String, rootName: String) { fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName)) val newDatabase = DatabaseKDBX(databaseName, rootName)
newDatabase.loadedCipherKey = LoadedKey.generateNewCipherKey()
setDatabaseKDBX(newDatabase)
this.fileUri = databaseUri this.fileUri = databaseUri
// Set Database state // Set Database state
this.loaded = true this.loaded = true
} }
class LoadedKey(val key: Key, val iv: IvParameterSpec) {
companion object {
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
fun generateNewCipherKey(): LoadedKey {
val iv = ByteArray(8)
SecureRandom().nextBytes(iv)
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), IvParameterSpec(iv))
}
}
}
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
openDatabaseKDB: (InputStream) -> DatabaseKDB, openDatabaseKDB: (InputStream) -> DatabaseKDB,
@@ -404,6 +435,7 @@ class Database {
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
mainCredential.masterPassword, mainCredential.masterPassword,
keyFileInputStream, keyFileInputStream,
LoadedKey.generateNewCipherKey(),
progressTaskUpdater, progressTaskUpdater,
fixDuplicateUUID) fixDuplicateUUID)
}, },
@@ -412,6 +444,7 @@ class Database {
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
mainCredential.masterPassword, mainCredential.masterPassword,
keyFileInputStream, keyFileInputStream,
LoadedKey.generateNewCipherKey(),
progressTaskUpdater, progressTaskUpdater,
fixDuplicateUUID) fixDuplicateUUID)
} }

View File

@@ -21,23 +21,31 @@ package com.kunzisoft.keepass.database.element.database
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.stream.readBytes import android.util.Base64
import android.util.Base64InputStream
import android.util.Base64OutputStream
import com.kunzisoft.keepass.database.element.Database
import org.apache.commons.io.output.CountingOutputStream
import java.io.* import java.io.*
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
class BinaryAttachment : Parcelable { class BinaryAttachment : Parcelable {
private var dataFile: File? = null private var dataFile: File? = null
var length: Long = 0
private set
var isCompressed: Boolean = false var isCompressed: Boolean = false
private set private set
var isProtected: Boolean = false var isProtected: Boolean = false
private set private set
var isCorrupted: Boolean = false var isCorrupted: Boolean = false
// Cipher to encrypt temp file
fun length(): Long { private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
return dataFile?.length() ?: 0 private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
}
/** /**
* Empty protected binary * Empty protected binary
@@ -46,6 +54,7 @@ class BinaryAttachment : Parcelable {
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) { constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
this.dataFile = dataFile this.dataFile = dataFile
this.length = 0
this.isCompressed = compressed this.isCompressed = compressed
this.isProtected = protected this.isProtected = protected
} }
@@ -54,58 +63,75 @@ class BinaryAttachment : Parcelable {
parcel.readString()?.let { parcel.readString()?.let {
dataFile = File(it) dataFile = File(it)
} }
length = parcel.readLong()
isCompressed = parcel.readByte().toInt() != 0 isCompressed = parcel.readByte().toInt() != 0
isProtected = parcel.readByte().toInt() != 0 isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0 isCorrupted = parcel.readByte().toInt() != 0
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getInputDataStream(): InputStream { fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return buildInputStream(dataFile!!, cipherKey)
}
@Throws(IOException::class)
fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return buildOutputStream(dataFile!!, cipherKey)
}
@Throws(IOException::class)
fun getUnGzipInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return if (isCompressed) {
GZIPInputStream(getInputDataStream(cipherKey))
} else {
getInputDataStream(cipherKey)
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream(cipherKey))
} else {
getOutputDataStream(cipherKey)
}
}
@Throws(IOException::class)
private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream {
return when { return when {
length() > 0 -> FileInputStream(dataFile!!) file != null && file.length() > 0 -> {
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, cipherKey.iv)
Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP)
}
else -> ByteArrayInputStream(ByteArray(0)) else -> ByteArrayInputStream(ByteArray(0))
} }
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getUnGzipInputDataStream(): InputStream { private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream {
return if (isCompressed)
GZIPInputStream(getInputDataStream())
else
getInputDataStream()
}
@Throws(IOException::class)
fun getOutputDataStream(): OutputStream {
return when { return when {
dataFile != null -> FileOutputStream(dataFile!!) file != null -> {
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, cipherKey.iv)
BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP))
}
else -> throw IOException("Unable to write in an unknown file") else -> throw IOException("Unable to write in an unknown file")
} }
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getGzipOutputDataStream(): OutputStream { fun compress(cipherKey: Database.LoadedKey, bufferSize: Int = DEFAULT_BUFFER_SIZE) {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream())
} else {
getOutputDataStream()
}
}
@Throws(IOException::class)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile -> dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file // To compress, create a new binary with file
if (!isCompressed) { if (!isCompressed) {
// Encrypt the new gzipped temp file
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream -> getInputDataStream(cipherKey).use { inputStream ->
getInputDataStream().use { inputStream -> GZIPOutputStream(buildOutputStream(fileBinaryCompress, cipherKey)).use { outputStream ->
inputStream.readBytes(bufferSize) { buffer -> inputStream.copyTo(outputStream, bufferSize)
outputStream.write(buffer)
} }
} }
} // Remove ungzip file
// Remove unGzip file
if (concreteDataFile.delete()) { if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) { if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression // Harmonize with database compression
@@ -117,15 +143,14 @@ class BinaryAttachment : Parcelable {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) { fun decompress(cipherKey: Database.LoadedKey, bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile -> dataFile?.let { concreteDataFile ->
if (isCompressed) { if (isCompressed) {
// Encrypt the new ungzipped temp file
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
FileOutputStream(fileBinaryDecompress).use { outputStream -> getUnGzipInputDataStream(cipherKey).use { inputStream ->
getUnGzipInputDataStream().use { inputStream -> buildOutputStream(fileBinaryDecompress, cipherKey).use { outputStream ->
inputStream.readBytes(bufferSize) { buffer -> inputStream.copyTo(outputStream, bufferSize)
outputStream.write(buffer)
}
} }
} }
// Remove gzip file // Remove gzip file
@@ -170,6 +195,7 @@ class BinaryAttachment : Parcelable {
result = 31 * result + if (isProtected) 1 else 0 result = 31 * result + if (isProtected) 1 else 0
result = 31 * result + if (isCorrupted) 1 else 0 result = 31 * result + if (isCorrupted) 1 else 0
result = 31 * result + dataFile!!.hashCode() result = 31 * result + dataFile!!.hashCode()
result = 31 * result + length.hashCode()
return result return result
} }
@@ -183,11 +209,31 @@ class BinaryAttachment : Parcelable {
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(dataFile?.absolutePath) dest.writeString(dataFile?.absolutePath)
dest.writeLong(length)
dest.writeByte((if (isCompressed) 1 else 0).toByte()) dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte()) dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeByte((if (isCorrupted) 1 else 0).toByte()) dest.writeByte((if (isCorrupted) 1 else 0).toByte())
} }
/**
* Custom OutputStream to calculate the size of binary file
*/
private inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
init {
length = 0
}
override fun beforeWrite(n: Int) {
super.beforeWrite(n)
length = byteCount
}
override fun close() {
super.close()
length = byteCount
}
}
companion object { companion object {
private val TAG = BinaryAttachment::class.java.name private val TAG = BinaryAttachment::class.java.name

View File

@@ -213,8 +213,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private fun compressAllBinaries() { private fun compressAllBinaries() {
binaryPool.doForEachBinary { binary -> binaryPool.doForEachBinary { binary ->
try { try {
val cipherKey = loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to compress binaries")
// To compress, create a new binary with file // To compress, create a new binary with file
binary.compress(BUFFER_SIZE_BYTES) binary.compress(cipherKey, BUFFER_SIZE_BYTES)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to compress $binary", e) Log.e(TAG, "Unable to compress $binary", e)
} }
@@ -224,7 +226,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private fun decompressAllBinaries() { private fun decompressAllBinaries() {
binaryPool.doForEachBinary { binary -> binaryPool.doForEachBinary { binary ->
try { try {
binary.decompress(BUFFER_SIZE_BYTES) val cipherKey = loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to decompress binaries")
binary.decompress(cipherKey, BUFFER_SIZE_BYTES)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to decompress $binary", e) Log.e(TAG, "Unable to decompress $binary", e)
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.entry.EntryVersioned import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.group.GroupVersioned import com.kunzisoft.keepass.database.element.group.GroupVersioned
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImageFactory
@@ -84,13 +85,13 @@ abstract class DatabaseVersioned<
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@Throws(IOException::class) @Throws(IOException::class)
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) { fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
masterKey = getMasterKey(key, keyInputStream) masterKey = getMasterKey(key, keyfileInputStream)
} }
@Throws(IOException::class) @Throws(IOException::class)
protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray { protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
val fileKey = getFileKey(keyInputStream) val fileKey = getFileKey(keyfileInputStream)
val passwordKey = getPasswordKey(key) val passwordKey = getPasswordKey(key)
val messageDigest: MessageDigest val messageDigest: MessageDigest
@@ -383,6 +384,12 @@ abstract class DatabaseVersioned<
return true return true
} }
/**
* Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements
*/
var loadedCipherKey: Database.LoadedKey? = null
companion object { companion object {
private const val TAG = "DatabaseVersioned" private const val TAG = "DatabaseVersioned"

View File

@@ -316,7 +316,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
var size = 0L var size = 0L
for ((label, poolId) in binaries) { for ((label, poolId) in binaries) {
size += label.length.toLong() size += label.length.toLong()
size += binaryPool[poolId]?.length() ?: 0 size += binaryPool[poolId]?.length ?: 0
} }
return size return size
} }

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.database.file.input package com.kunzisoft.keepass.database.file.input
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -40,7 +41,7 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyfileInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb fixDuplicateUUID: Boolean = false): PwDb
@@ -50,4 +51,20 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
masterKey: ByteArray, masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb fixDuplicateUUID: Boolean = false): PwDb
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
} }

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.file.input
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -48,16 +49,16 @@ import javax.crypto.spec.SecretKeySpec
class DatabaseInputKDB(cacheDirectory: File) class DatabaseInputKDB(cacheDirectory: File)
: DatabaseInput<DatabaseKDB>(cacheDirectory) { : DatabaseInput<DatabaseKDB>(cacheDirectory) {
private lateinit var mDatabaseToOpen: DatabaseKDB private lateinit var mDatabase: DatabaseKDB
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyfileInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB { fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream) mDatabase.retrieveMasterKey(password, keyfileInputStream)
} }
} }
@@ -67,7 +68,33 @@ class DatabaseInputKDB(cacheDirectory: File)
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB { fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.masterKey = masterKey mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey
} }
} }
@@ -101,38 +128,38 @@ class DatabaseInputKDB(cacheDirectory: File)
} }
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
mDatabaseToOpen = DatabaseKDB() mDatabase = DatabaseKDB()
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID mDatabase.changeDuplicateId = fixDuplicateUUID
assignMasterKey?.invoke() assignMasterKey?.invoke()
// Select algorithm // Select algorithm
when { when {
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> { header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> {
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael mDatabase.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
} }
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt() != 0 -> { header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt() != 0 -> {
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish mDatabase.encryptionAlgorithm = EncryptionAlgorithm.Twofish
} }
else -> throw InvalidAlgorithmDatabaseException() else -> throw InvalidAlgorithmDatabaseException()
} }
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong() mDatabase.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong()
// Generate transformedMasterKey from masterKey // Generate transformedMasterKey from masterKey
mDatabaseToOpen.makeFinalKey( mDatabase.makeFinalKey(
header.masterSeed, header.masterSeed,
header.transformSeed, header.transformSeed,
mDatabaseToOpen.numberKeyEncryptionRounds) mDatabase.numberKeyEncryptionRounds)
progressTaskUpdater?.updateMessage(R.string.decrypting_db) progressTaskUpdater?.updateMessage(R.string.decrypting_db)
// Initialize Rijndael algorithm // Initialize Rijndael algorithm
val cipher: Cipher = try { val cipher: Cipher = try {
when { when {
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> { mDatabase.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
CipherFactory.getInstance("AES/CBC/PKCS5Padding") CipherFactory.getInstance("AES/CBC/PKCS5Padding")
} }
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> { mDatabase.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING") CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
} }
else -> throw IOException("Encryption algorithm is not supported") else -> throw IOException("Encryption algorithm is not supported")
@@ -145,7 +172,7 @@ class DatabaseInputKDB(cacheDirectory: File)
try { try {
cipher.init(Cipher.DECRYPT_MODE, cipher.init(Cipher.DECRYPT_MODE,
SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), SecretKeySpec(mDatabase.finalKey, "AES"),
IvParameterSpec(header.encryptionIV)) IvParameterSpec(header.encryptionIV))
} catch (e1: InvalidKeyException) { } catch (e1: InvalidKeyException) {
throw IOException("Invalid key") throw IOException("Invalid key")
@@ -169,9 +196,9 @@ class DatabaseInputKDB(cacheDirectory: File)
) )
// New manual root because KDB contains multiple root groups (here available with getRootGroups()) // New manual root because KDB contains multiple root groups (here available with getRootGroups())
val newRoot = mDatabaseToOpen.createGroup() val newRoot = mDatabase.createGroup()
newRoot.level = -1 newRoot.level = -1
mDatabaseToOpen.rootGroup = newRoot mDatabase.rootGroup = newRoot
// Import all nodes // Import all nodes
var newGroup: GroupKDB? = null var newGroup: GroupKDB? = null
@@ -192,12 +219,12 @@ class DatabaseInputKDB(cacheDirectory: File)
// Create new node depending on byte number // Create new node depending on byte number
when (fieldSize) { when (fieldSize) {
4 -> { 4 -> {
newGroup = mDatabaseToOpen.createGroup().apply { newGroup = mDatabase.createGroup().apply {
setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt()) setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt())
} }
} }
16 -> { 16 -> {
newEntry = mDatabaseToOpen.createEntry().apply { newEntry = mDatabase.createEntry().apply {
nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid()) nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid())
} }
} }
@@ -211,7 +238,7 @@ class DatabaseInputKDB(cacheDirectory: File)
group.title = cipherInputStream.readBytesToString(fieldSize) group.title = cipherInputStream.readBytesToString(fieldSize)
} ?: } ?:
newEntry?.let { entry -> newEntry?.let { entry ->
val groupKDB = mDatabaseToOpen.createGroup() val groupKDB = mDatabase.createGroup()
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt()) groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt())
entry.parent = groupKDB entry.parent = groupKDB
} }
@@ -226,7 +253,7 @@ class DatabaseInputKDB(cacheDirectory: File)
if (iconId == -1) { if (iconId == -1) {
iconId = 0 iconId = 0
} }
entry.icon = mDatabaseToOpen.iconFactory.getIcon(iconId) entry.icon = mDatabase.iconFactory.getIcon(iconId)
} }
} }
0x0004 -> { 0x0004 -> {
@@ -255,7 +282,7 @@ class DatabaseInputKDB(cacheDirectory: File)
} }
0x0007 -> { 0x0007 -> {
newGroup?.let { group -> newGroup?.let { group ->
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt()) group.icon = mDatabase.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
} ?: } ?:
newEntry?.let { entry -> newEntry?.let { entry ->
entry.password = cipherInputStream.readBytesToString(fieldSize,false) entry.password = cipherInputStream.readBytesToString(fieldSize,false)
@@ -300,13 +327,12 @@ class DatabaseInputKDB(cacheDirectory: File)
0x000E -> { 0x000E -> {
newEntry?.let { entry -> newEntry?.let { entry ->
if (fieldSize > 0) { if (fieldSize > 0) {
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory) val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory)
entry.binaryData = binaryAttachment entry.binaryData = binaryAttachment
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream -> val cipherKey = mDatabase.loadedCipherKey
cipherInputStream.readBytes(fieldSize, ?: throw IOException("Unable to retrieve cipher key to load binaries")
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer -> BufferedOutputStream(binaryAttachment.getOutputDataStream(cipherKey)).use { outputStream ->
outputStream.write(buffer) cipherInputStream.copyPartTo(outputStream, fieldSize)
}
} }
} }
} }
@@ -314,12 +340,12 @@ class DatabaseInputKDB(cacheDirectory: File)
0xFFFF -> { 0xFFFF -> {
// End record. Save node and count it. // End record. Save node and count it.
newGroup?.let { group -> newGroup?.let { group ->
mDatabaseToOpen.addGroupIndex(group) mDatabase.addGroupIndex(group)
currentGroupNumber++ currentGroupNumber++
newGroup = null newGroup = null
} }
newEntry?.let { entry -> newEntry?.let { entry ->
mDatabaseToOpen.addEntryIndex(entry) mDatabase.addEntryIndex(entry)
currentEntryNumber++ currentEntryNumber++
newEntry = null newEntry = null
} }
@@ -337,20 +363,20 @@ class DatabaseInputKDB(cacheDirectory: File)
constructTreeFromIndex() constructTreeFromIndex()
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
mDatabaseToOpen.clearCache() mDatabase.clearCache()
throw e throw e
} catch (e: IOException) { } catch (e: IOException) {
mDatabaseToOpen.clearCache() mDatabase.clearCache()
throw IODatabaseException(e) throw IODatabaseException(e)
} catch (e: OutOfMemoryError) { } catch (e: OutOfMemoryError) {
mDatabaseToOpen.clearCache() mDatabase.clearCache()
throw NoMemoryDatabaseException(e) throw NoMemoryDatabaseException(e)
} catch (e: Exception) { } catch (e: Exception) {
mDatabaseToOpen.clearCache() mDatabase.clearCache()
throw LoadDatabaseException(e) throw LoadDatabaseException(e)
} }
return mDatabaseToOpen return mDatabase
} }
private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator<GroupKDB>) { private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator<GroupKDB>) {
@@ -375,18 +401,18 @@ class DatabaseInputKDB(cacheDirectory: File)
} }
private fun constructTreeFromIndex() { private fun constructTreeFromIndex() {
mDatabaseToOpen.rootGroup?.let { mDatabase.rootGroup?.let {
// add each group // add each group
val groupIterator = mDatabaseToOpen.getGroupIndexes().iterator() val groupIterator = mDatabase.getGroupIndexes().iterator()
if (groupIterator.hasNext()) if (groupIterator.hasNext())
buildTreeGroups(it, groupIterator.next(), groupIterator) buildTreeGroups(it, groupIterator.next(), groupIterator)
// add each child // add each child
for (currentEntry in mDatabaseToOpen.getEntryIndexes()) { for (currentEntry in mDatabase.getEntryIndexes()) {
if (currentEntry.parent != null) { if (currentEntry.parent != null) {
// Only the parent id is known so complete the info // Only the parent id is known so complete the info
val parentGroupRetrieve = mDatabaseToOpen.getGroupById(currentEntry.parent!!.nodeId) val parentGroupRetrieve = mDatabase.getGroupById(currentEntry.parent!!.nodeId)
parentGroupRetrieve?.addChildEntry(currentEntry) parentGroupRetrieve?.addChildEntry(currentEntry)
currentEntry.parent = parentGroupRetrieve currentEntry.parent = parentGroupRetrieve
} }

View File

@@ -26,12 +26,14 @@ import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
@@ -96,11 +98,11 @@ class DatabaseInputKDBX(cacheDirectory: File)
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyfileInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX { fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.retrieveMasterKey(password, keyInputStream) mDatabase.retrieveMasterKey(password, keyfileInputStream)
} }
} }
@@ -114,6 +116,31 @@ class DatabaseInputKDBX(cacheDirectory: File)
} }
} }
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream, private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
@@ -273,10 +300,10 @@ class DatabaseInputKDBX(cacheDirectory: File)
val byteLength = size - 1 val byteLength = size - 1
// No compression at this level // No compression at this level
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag) val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag)
protectedBinary.getOutputDataStream().use { outputStream -> val cipherKey = mDatabase.loadedCipherKey
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer -> ?: throw IOException("Unable to retrieve cipher key to load binaries")
outputStream.write(buffer) protectedBinary.getOutputDataStream(cipherKey).use { outputStream ->
} dataInputStream.copyPartTo(outputStream, byteLength)
} }
} }
} }
@@ -1009,14 +1036,16 @@ class DatabaseInputKDBX(cacheDirectory: File)
// Build the new binary and compress // Build the new binary and compress
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId) val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId)
val binaryCipherKey = mDatabase.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to load binaries")
try { try {
binaryAttachment.getOutputDataStream().use { outputStream -> binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
outputStream.write(Base64.decode(base64, BASE_64_FLAG)) outputStream.write(Base64.decode(base64, BASE_64_FLAG))
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to read base 64 attachment", e) Log.e(TAG, "Unable to read base 64 attachment", e)
binaryAttachment.isCorrupted = true binaryAttachment.isCorrupted = true
binaryAttachment.getOutputDataStream().use { outputStream -> binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
outputStream.write(base64.toByteArray()) outputStream.write(base64.toByteArray())
} }
} }
@@ -1083,6 +1112,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
return xpp return xpp
} }
} }
} }
@Throws(IOException::class, XmlPullParserException::class) @Throws(IOException::class, XmlPullParserException::class)

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
@@ -197,15 +197,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
@Suppress("CAST_NEVER_SUCCEEDS") @Suppress("CAST_NEVER_SUCCEEDS")
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
fun outputPlanGroupAndEntries(os: OutputStream) { fun outputPlanGroupAndEntries(outputStream: OutputStream) {
val los = LittleEndianDataOutputStream(os) val littleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
// useHeaderHash // useHeaderHash
if (headerHashBlock != null) { if (headerHashBlock != null) {
try { try {
los.writeUShort(0x0000) littleEndianDataOutputStream.writeUShort(0x0000)
los.writeInt(headerHashBlock!!.size) littleEndianDataOutputStream.writeInt(headerHashBlock!!.size)
los.write(headerHashBlock!!) littleEndianDataOutputStream.write(headerHashBlock!!)
} catch (e: IOException) { } catch (e: IOException) {
throw DatabaseOutputException("Failed to output header hash.", e) throw DatabaseOutputException("Failed to output header hash.", e)
} }
@@ -213,20 +213,12 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
// Groups // Groups
mDatabaseKDB.doForEachGroupInIndex { group -> mDatabaseKDB.doForEachGroupInIndex { group ->
val pgo = GroupOutputKDB(group, os) GroupOutputKDB.write(outputStream, group)
try {
pgo.output()
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output a tree", e)
}
} }
mDatabaseKDB.doForEachEntryInIndex { entry -> mDatabaseKDB.doForEachEntryInIndex { entry ->
val peo = EntryOutputKDB(entry, os) val binaryCipherKey = mDatabaseKDB.loadedCipherKey
try { ?: throw DatabaseOutputException("Unable to retrieve cipher key to write binaries")
peo.output() EntryOutputKDB.writeEntry(outputStream, entry, binaryCipherKey)
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output an entry.", e)
}
} }
} }

View File

@@ -140,12 +140,14 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
database.binaryPool.doForEachOrderedBinary { _, keyBinary -> database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary val protectedBinary = keyBinary.binary
val binaryCipherKey = database.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to write binaries")
// Force decompression to add binary in header // Force decompression to add binary in header
protectedBinary.decompress() protectedBinary.decompress(binaryCipherKey)
// Write type binary // Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size // Write size
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1)) dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length + 1))
// Write protected flag // Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) { if (protectedBinary.isProtected) {
@@ -153,10 +155,8 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
dataOutputStream.writeByte(flag) dataOutputStream.writeByte(flag)
protectedBinary.getInputDataStream().use { inputStream -> protectedBinary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> inputStream.copyTo(dataOutputStream)
dataOutputStream.write(buffer)
}
} }
} }
@@ -502,13 +502,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemBinary) xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString()) xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
val binary = keyBinary.binary val binary = keyBinary.binary
if (binary.length() > 0) { if (binary.length > 0) {
if (binary.isCompressed) { if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue) xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
} }
// Write the XML // Write the XML
binary.getInputDataStream().use { inputStream -> val binaryCipherKey = mDatabaseKDBX.loadedCipherKey
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> ?: throw IOException("Unable to retrieve cipher key to write binaries")
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes(BUFFER_SIZE_BYTES) { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
} }
} }

View File

@@ -19,9 +19,10 @@
*/ */
package com.kunzisoft.keepass.database.file.output package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.file.output.GroupOutputKDB.Companion.GROUPID_FIELD_SIZE import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.stream.* import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
@@ -29,24 +30,18 @@ import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.nio.charset.Charset import java.nio.charset.Charset
class EntryOutputKDB
/** /**
* Output the GroupKDB to the stream * Output the GroupKDB to the stream
*/ */
(private val mEntry: EntryKDB, private val mOutputStream: OutputStream) { class EntryOutputKDB {
/**
* Returns the number of bytes written by the stream
* @return Number of bytes written
*/
var length: Long = 0
private set
companion object {
@Throws(DatabaseOutputException::class)
fun writeEntry(mOutputStream: OutputStream,
mEntry: EntryKDB,
binaryCipherKey: Database.LoadedKey) {
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int //NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int
@Throws(IOException::class) try {
fun output() {
length += 134 // Length of fixed size fields
// UUID // UUID
mOutputStream.write(UUID_FIELD_TYPE) mOutputStream.write(UUID_FIELD_TYPE)
mOutputStream.write(UUID_FIELD_SIZE) mOutputStream.write(UUID_FIELD_SIZE)
@@ -65,51 +60,50 @@ class EntryOutputKDB
// Title // Title
//byte[] title = mEntry.title.getBytes("UTF-8"); //byte[] title = mEntry.title.getBytes("UTF-8");
mOutputStream.write(TITLE_FIELD_TYPE) mOutputStream.write(TITLE_FIELD_TYPE)
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.title, mOutputStream).toLong() StringDatabaseKDBUtils.writeStringToBytes(mEntry.title, mOutputStream).toLong()
// URL // URL
mOutputStream.write(URL_FIELD_TYPE) mOutputStream.write(URL_FIELD_TYPE)
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.url, mOutputStream).toLong() StringDatabaseKDBUtils.writeStringToBytes(mEntry.url, mOutputStream).toLong()
// Username // Username
mOutputStream.write(USERNAME_FIELD_TYPE) mOutputStream.write(USERNAME_FIELD_TYPE)
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.username, mOutputStream).toLong() StringDatabaseKDBUtils.writeStringToBytes(mEntry.username, mOutputStream).toLong()
// Password // Password
mOutputStream.write(PASSWORD_FIELD_TYPE) mOutputStream.write(PASSWORD_FIELD_TYPE)
length += writePassword(mEntry.password, mOutputStream).toLong() writePassword(mOutputStream, mEntry.password).toLong()
// Additional // Additional
mOutputStream.write(ADDITIONAL_FIELD_TYPE) mOutputStream.write(ADDITIONAL_FIELD_TYPE)
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.notes, mOutputStream).toLong() StringDatabaseKDBUtils.writeStringToBytes(mEntry.notes, mOutputStream).toLong()
// Create date // Create date
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date)) writeDate(mOutputStream, CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
// Modification date // Modification date
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date)) writeDate(mOutputStream, MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
// Access date // Access date
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date)) writeDate(mOutputStream, ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
// Expiration date // Expiration date
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date)) writeDate(mOutputStream, EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
// Binary description // Binary description
mOutputStream.write(BINARY_DESC_FIELD_TYPE) mOutputStream.write(BINARY_DESC_FIELD_TYPE)
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.binaryDescription, mOutputStream).toLong() StringDatabaseKDBUtils.writeStringToBytes(mEntry.binaryDescription, mOutputStream).toLong()
// Binary // Binary
mOutputStream.write(BINARY_DATA_FIELD_TYPE) mOutputStream.write(BINARY_DATA_FIELD_TYPE)
val binaryData = mEntry.binaryData val binaryData = mEntry.binaryData
val binaryDataLength = binaryData?.length() ?: 0L val binaryDataLength = binaryData?.length ?: 0L
// Write data length // Write data length
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength))) mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
// Write data // Write data
if (binaryDataLength > 0) { if (binaryDataLength > 0) {
binaryData?.getInputDataStream().use { inputStream -> binaryData?.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream?.readBytes(DatabaseKDB.BUFFER_SIZE_BYTES) { buffer -> inputStream?.readAllBytes(DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
length += buffer.size
mOutputStream.write(buffer) mOutputStream.write(buffer)
} }
inputStream?.close() inputStream?.close()
@@ -119,54 +113,59 @@ class EntryOutputKDB
// End // End
mOutputStream.write(END_FIELD_TYPE) mOutputStream.write(END_FIELD_TYPE)
mOutputStream.write(ZERO_FIELD_SIZE) mOutputStream.write(ZERO_FIELD_SIZE)
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output an entry.", e)
}
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun writeDate(type: ByteArray, date: ByteArray?) { private fun writeDate(outputStream: OutputStream,
mOutputStream.write(type) type: ByteArray,
mOutputStream.write(DATE_FIELD_SIZE) date: ByteArray?) {
outputStream.write(type)
outputStream.write(DATE_FIELD_SIZE)
if (date != null) { if (date != null) {
mOutputStream.write(date) outputStream.write(date)
} else { } else {
mOutputStream.write(ZERO_FIVE) outputStream.write(ZERO_FIVE)
} }
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun writePassword(str: String, os: OutputStream): Int { private fun writePassword(outputStream: OutputStream, str: String): Int {
val initial = str.toByteArray(Charset.forName("UTF-8")) val initial = str.toByteArray(Charset.forName("UTF-8"))
val length = initial.size + 1 val length = initial.size + 1
os.write(uIntTo4Bytes(UnsignedInt(length))) outputStream.write(uIntTo4Bytes(UnsignedInt(length)))
os.write(initial) outputStream.write(initial)
os.write(0x00) outputStream.write(0x00)
return length return length
} }
companion object {
// Constants // Constants
val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1) private val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) private val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1)
val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3) private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4) private val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5) private val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6) private val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7) private val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8) private val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9) private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10) private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10)
val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11) private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11)
val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12) private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12)
val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13) private val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13)
val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14) private val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14)
val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF) private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
val LONG_FOUR:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val LONG_FOUR:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16)) private val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16))
val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5)) private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0)) private val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00) private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
private val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00)
} }
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.file.output package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.stream.dateTo5Bytes import com.kunzisoft.keepass.stream.dateTo5Bytes
import com.kunzisoft.keepass.stream.uIntTo4Bytes import com.kunzisoft.keepass.stream.uIntTo4Bytes
import com.kunzisoft.keepass.stream.uShortTo2Bytes import com.kunzisoft.keepass.stream.uShortTo2Bytes
@@ -31,12 +32,14 @@ import java.io.OutputStream
/** /**
* Output the GroupKDB to the stream * Output the GroupKDB to the stream
*/ */
class GroupOutputKDB (private val mGroup: GroupKDB, private val mOutputStream: OutputStream) { class GroupOutputKDB {
@Throws(IOException::class) companion object {
fun output() { @Throws(DatabaseOutputException::class)
fun write(mOutputStream: OutputStream,
mGroup: GroupKDB) {
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int, but most values can't be greater than 2^31, so it probably doesn't matter. //NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int, but most values can't be greater than 2^31, so it probably doesn't matter.
try {
// Group ID // Group ID
mOutputStream.write(GROUPID_FIELD_TYPE) mOutputStream.write(GROUPID_FIELD_TYPE)
mOutputStream.write(GROUPID_FIELD_SIZE) mOutputStream.write(GROUPID_FIELD_SIZE)
@@ -84,26 +87,28 @@ class GroupOutputKDB (private val mGroup: GroupKDB, private val mOutputStream: O
// End // End
mOutputStream.write(END_FIELD_TYPE) mOutputStream.write(END_FIELD_TYPE)
mOutputStream.write(ZERO_FIELD_SIZE) mOutputStream.write(ZERO_FIELD_SIZE)
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output a group", e)
}
} }
companion object {
// Constants // Constants
val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1) private val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1)
val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) private val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3) private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4) private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5) private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6) private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7) private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8) private val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9) private val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF) private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5)) private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2)) private val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2))
val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0)) private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
} }
} }

View File

@@ -29,11 +29,12 @@ import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.stream.readBytes import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -345,7 +346,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to upload or download file", e) e.printStackTrace()
progressResult = false progressResult = false
} }
progressResult progressResult
@@ -372,10 +373,11 @@ class AttachmentFileNotificationService: LockNotificationService() {
bufferSize: Int = DEFAULT_BUFFER_SIZE, bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) { update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0L var dataDownloaded = 0L
val fileSize = binaryAttachment.length() val fileSize = binaryAttachment.length
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream -> UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
binaryAttachment.getUnGzipInputDataStream().use { inputStream -> Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
inputStream.readBytes(bufferSize) { buffer -> binaryAttachment.getUnGzipInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes(bufferSize) { buffer ->
outputStream.write(buffer) outputStream.write(buffer)
dataDownloaded += buffer.size dataDownloaded += buffer.size
try { try {
@@ -388,6 +390,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
} }
} }
} }
}
fun uploadToDatabase(attachmentFromDownloadUri: Uri, fun uploadToDatabase(attachmentFromDownloadUri: Uri,
binaryAttachment: BinaryAttachment, binaryAttachment: BinaryAttachment,
@@ -396,10 +399,10 @@ class AttachmentFileNotificationService: LockNotificationService() {
update: ((percent: Int)->Unit)? = null) { update: ((percent: Int)->Unit)? = null) {
var dataUploaded = 0L var dataUploaded = 0L
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0 val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.let { inputStream -> UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.use { inputStream ->
binaryAttachment.getGzipOutputDataStream().use { outputStream -> Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
BufferedInputStream(inputStream).use { attachmentBufferedInputStream -> binaryAttachment.getGzipOutputDataStream(binaryCipherKey).use { outputStream ->
attachmentBufferedInputStream.readBytes(bufferSize) { buffer -> inputStream.readAllBytes(bufferSize) { buffer ->
outputStream.write(buffer) outputStream.write(buffer)
dataUploaded += buffer.size dataUploaded += buffer.size
try { try {

View File

@@ -24,13 +24,14 @@ import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils.bytesToString
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
import java.util.* import java.util.*
/** /**
* Read all data of stream and invoke [readBytes] each time the buffer is full or no more data to read. * Read all data of stream and invoke [readBytes] each time the buffer is full or no more data to read.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun InputStream.readBytes(bufferSize: Int, readBytes: (bytesRead: ByteArray) -> Unit) { fun InputStream.readAllBytes(bufferSize: Int, readBytes: (bytesRead: ByteArray) -> Unit) {
val buffer = ByteArray(bufferSize) val buffer = ByteArray(bufferSize)
var read = 0 var read = 0
while (read != -1) { while (read != -1) {
@@ -47,32 +48,17 @@ fun InputStream.readBytes(bufferSize: Int, readBytes: (bytesRead: ByteArray) ->
} }
/** /**
* Read number of bytes defined by [length] and invoke [readBytes] each time the buffer is full or no more data to read. * Read number of bytes defined by [length] and copy the content in [outputStream]
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun InputStream.readBytes(length: Int, bufferSize: Int, readBytes: (bytesRead: ByteArray) -> Unit) { fun InputStream.copyPartTo(outputStream: OutputStream, length: Int, bufferSize: Int = DEFAULT_BUFFER_SIZE) {
var bufferLength = bufferSize var bytesCopied: Long = 0
var buffer = ByteArray(bufferLength) val buffer = ByteArray(bufferSize)
var bytesRead = read(buffer)
var offset = 0 while (bytesRead >= 0 && bytesCopied <= length) {
var read = 0 outputStream.write(buffer, 0, bytesRead)
while (offset < length && read != -1) { bytesCopied += bytesRead
bytesRead = read(buffer)
// To reduce the buffer for the last bytes reads
if (length - offset < bufferLength) {
bufferLength = length - offset
buffer = ByteArray(bufferLength)
}
read = this.read(buffer, 0, bufferLength)
// To get only the bytes read
val optimizedBuffer: ByteArray = if (read >= 0 && buffer.size > read) {
buffer.copyOf(read)
} else {
buffer
}
readBytes.invoke(optimizedBuffer)
offset += read
} }
} }