mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'feature/Encrypt_Temp_Binaries' into feature/Image_Viewer
This commit is contained in:
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/androidTest/assets/test_image.png
Normal file
BIN
app/src/androidTest/assets/test_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
133
app/src/androidTest/assets/test_text.txt
Normal file
133
app/src/androidTest/assets/test_text.txt
Normal 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
|
||||||
|
℀ ℁ ℂ ℃ ℄ ℅ ℆ ℇ ℈ ℉ ℊ ℋ ℌ ℍ ℎ ℏ ℐ ℑ ℒ ℓ ℔ ℕ № ℗ ℘ ℙ ℚ ℛ ℜ ℝ ℞ ℟ ℠ ℡ ™ ℣ ℤ ℥ Ω ℧ ℨ ℩ K Å ℬ ℭ ℮ ℯ ℰ ℱ Ⅎ ℳ ℴ ℵ ℶ ℷ ℸ
|
||||||
|
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
|
||||||
|
! " # $ % & ' ( ) * + , - . / 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 { | } ~ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ ...
|
||||||
|
Specials
|
||||||
|
|
||||||
|
Specials
|
||||||
|
<20>
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user