Key files can be opened from deliberate locations

TODO: fix a problem with .kdb-files and key files

Added very basic and not yet functional AndroidContentStorage.cs
This commit is contained in:
Philipp Crocoll 2014-11-08 21:29:36 +01:00
parent 2e4c3e3490
commit 3239131a84
24 changed files with 3636 additions and 3450 deletions

2
.gitignore vendored
View File

@ -282,3 +282,5 @@ Thumbs.db
/src/java/MasterKee
/src/PluginSdkBinding/obj/ReleaseNoNet
/src/MasterKeeWinPlugin/bin/Release
/src/SamplePlugin

View File

@ -20,7 +20,7 @@
<DebugType>full</DebugType>
<Optimize>False</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;EXCLUDE_TWOFISH;INCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;EXCLUDE_KEYTRANSFORM</DefineConstants>
<DefineConstants>DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>False</ConsolePause>

View File

@ -41,7 +41,7 @@ namespace KeePassLib.Keys
/// </summary>
public sealed class KcpKeyFile : IUserKey
{
private string m_strPath;
private IOConnectionInfo m_ioc;
private ProtectedBinary m_pbKeyData;
/// <summary>
@ -49,7 +49,7 @@ namespace KeePassLib.Keys
/// </summary>
public string Path
{
get { return m_strPath; }
get { return m_ioc.Path; }
}
/// <summary>
@ -62,6 +62,11 @@ namespace KeePassLib.Keys
get { return m_pbKeyData; }
}
public IOConnectionInfo Ioc
{
get { return m_ioc; }
}
public KcpKeyFile(string strKeyFile)
{
Construct(IOConnectionInfo.FromPath(strKeyFile), false);
@ -82,17 +87,21 @@ namespace KeePassLib.Keys
Construct(iocKeyFile, bThrowIfDbFile);
}
private void Construct(IOConnectionInfo iocFile, bool bThrowIfDbFile)
public KcpKeyFile(byte[] keyFileContents, IOConnectionInfo iocKeyFile, bool bThrowIfDbFile)
{
byte[] pbFileData = IOConnection.ReadFile(iocFile);
if(pbFileData == null) throw new Java.IO.FileNotFoundException();
Construct(keyFileContents, iocKeyFile, bThrowIfDbFile);
}
if(bThrowIfDbFile && (pbFileData.Length >= 8))
private void Construct(byte[] pbFileData, IOConnectionInfo iocKeyFile, bool bThrowIfDbFile)
{
if (pbFileData == null) throw new Java.IO.FileNotFoundException();
if (bThrowIfDbFile && (pbFileData.Length >= 8))
{
uint uSig1 = MemUtil.BytesToUInt32(MemUtil.Mid(pbFileData, 0, 4));
uint uSig2 = MemUtil.BytesToUInt32(MemUtil.Mid(pbFileData, 4, 4));
if(((uSig1 == KdbxFile.FileSignature1) &&
if (((uSig1 == KdbxFile.FileSignature1) &&
(uSig2 == KdbxFile.FileSignature2)) ||
((uSig1 == KdbxFile.FileSignaturePreRelease1) &&
(uSig2 == KdbxFile.FileSignaturePreRelease2)) ||
@ -106,16 +115,22 @@ namespace KeePassLib.Keys
}
byte[] pbKey = LoadXmlKeyFile(pbFileData);
if(pbKey == null) pbKey = LoadKeyFile(pbFileData);
if (pbKey == null) pbKey = LoadKeyFile(pbFileData);
if(pbKey == null) throw new InvalidOperationException();
if (pbKey == null) throw new InvalidOperationException();
m_strPath = iocFile.Path;
m_ioc = iocKeyFile;
m_pbKeyData = new ProtectedBinary(true, pbKey);
MemUtil.ZeroByteArray(pbKey);
}
private void Construct(IOConnectionInfo iocFile, bool bThrowIfDbFile)
{
byte[] pbFileData = IOConnection.ReadFile(iocFile);
Construct(pbFileData, iocFile, bThrowIfDbFile);
}
// public void Clear()
// {
// m_strPath = string.Empty;

View File

@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using KeePassLib.Serialization;
namespace keepass2android.Io
{
//TODOC,TOTEST, TODO: unimplemented methods?
public class AndroidContentStorage: IFileStorage
{
private readonly Context _ctx;
public AndroidContentStorage(Context ctx)
{
_ctx = ctx;
}
public IEnumerable<string> SupportedProtocols
{
get { yield return "content"; }
}
public void Delete(IOConnectionInfo ioc)
{
throw new NotImplementedException();
}
public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
{
return false;
}
public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
{
return null;
}
public Stream OpenFileForRead(IOConnectionInfo ioc)
{
return _ctx.ContentResolver.OpenInputStream(Android.Net.Uri.Parse(ioc.Path));
}
public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
{
return new AndroidContentWriteTransaction(ioc.Path, _ctx);
}
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
{
return "";
}
public bool RequiresCredentials(IOConnectionInfo ioc)
{
return false;
}
public void CreateDirectory(IOConnectionInfo ioc, string newDirName)
{
throw new NotImplementedException();
}
public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
{
throw new NotImplementedException();
}
public FileDescription GetFileDescription(IOConnectionInfo ioc)
{
throw new NotImplementedException();
}
public bool RequiresSetup(IOConnectionInfo ioConnection)
{
return false;
}
public string IocToPath(IOConnectionInfo ioc)
{
return ioc.Path;
}
public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId)
{
Intent intent = new Intent();
activity.IocToIntent(intent, new IOConnectionInfo() { Path = protocolId + "://" });
activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileChooserPrepared, intent);
}
public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode,
bool alwaysReturnSuccess)
{
Intent intent = new Intent();
activity.IocToIntent(intent, ioc);
activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileUsagePrepared, intent);
}
public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState)
{
throw new NotImplementedException();
}
public void OnResume(IFileStorageSetupActivity activity)
{
throw new NotImplementedException();
}
public void OnStart(IFileStorageSetupActivity activity)
{
throw new NotImplementedException();
}
public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data)
{
throw new NotImplementedException();
}
public string GetDisplayName(IOConnectionInfo ioc)
{
return ioc.Path;
}
public string CreateFilePath(string parent, string newFilename)
{
throw new NotImplementedException();
}
public IOConnectionInfo GetParentPath(IOConnectionInfo ioc)
{
throw new NotImplementedException();
}
public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename)
{
throw new NotImplementedException();
}
}
class AndroidContentWriteTransaction : IWriteTransaction
{
private readonly string _path;
private readonly Context _ctx;
private MemoryStream _memoryStream;
public AndroidContentWriteTransaction(string path, Context ctx)
{
_path = path;
_ctx = ctx;
}
public void Dispose()
{
_memoryStream.Dispose();
}
public Stream OpenFile()
{
_memoryStream = new MemoryStream();
return _memoryStream;
}
public void CommitWrite()
{
using (Stream outputStream = _ctx.ContentResolver.OpenOutputStream(Android.Net.Uri.Parse(_path)))
{
outputStream.Write(_memoryStream.ToArray(), 0, (int)_memoryStream.Length);
}
}
}
}

View File

@ -20,7 +20,7 @@
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>TRACE;DEBUG;EXCLUDE_TWOFISH;INCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;EXCLUDE_KEYTRANSFORM</DefineConstants>
<DefineConstants>TRACE;DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
@ -64,6 +64,7 @@
<Compile Include="DataExchange\Formats\KeePassKdb2x.cs" />
<Compile Include="DataExchange\Formats\KeePassXml2x.cs" />
<Compile Include="DataExchange\PwExportInfo.cs" />
<Compile Include="Io\AndroidContentStorage.cs" />
<Compile Include="Io\BuiltInFileStorage.cs" />
<Compile Include="Io\CachingFileStorage.cs" />
<Compile Include="Io\DropboxFileStorage.cs" />

View File

@ -11,5 +11,5 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-20
target=android-19
android.library=true

View File

@ -47,10 +47,14 @@ package com.keepassdroid.database;
// Java
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.io.UnsupportedEncodingException;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
@ -125,18 +129,18 @@ public class PwDatabaseV3 {
public void setMasterKey(String key, String keyFileName)
public void setMasterKey(String key, InputStream keyfileStream)
throws InvalidKeyFileException, IOException {
assert( key != null && keyFileName != null );
assert( key != null && keyfileStream != null );
masterKey = getMasterKey(key, keyFileName);
masterKey = getMasterKey(key, keyfileStream);
}
protected byte[] getCompositeKey(String key, String keyFileName)
protected byte[] getCompositeKey(String key, InputStream keyfileStream)
throws InvalidKeyFileException, IOException {
assert(key != null && keyFileName != null);
assert(key != null && keyfileStream != null);
byte[] fileKey = getFileKey(keyFileName);
byte[] fileKey = getFileKey(keyfileStream);
byte[] passwordKey = getPasswordKey(key);
@ -152,45 +156,39 @@ public class PwDatabaseV3 {
return md.digest(fileKey);
}
protected byte[] getFileKey(String fileName)
protected byte[] getFileKey(InputStream keyfileStream)
throws InvalidKeyFileException, IOException {
assert(fileName != null);
assert(keyfileStream != null);
File keyfile = new File(fileName);
if ( ! keyfile.exists() ) {
throw new InvalidKeyFileException();
byte[] buff = new byte[8000];
int bytesRead = 0;
ByteArrayOutputStream bao = new ByteArrayOutputStream();
while ((bytesRead = keyfileStream.read(buff)) != -1) {
bao.write(buff, 0, bytesRead);
}
byte[] key = loadXmlKeyFile(fileName);
if ( key != null ) {
return key;
}
byte[] keyFileData = bao.toByteArray();
FileInputStream fis;
try {
fis = new FileInputStream(keyfile);
} catch (FileNotFoundException e) {
throw new InvalidKeyFileException();
}
ByteArrayInputStream bin = new ByteArrayInputStream(keyFileData);
BufferedInputStream bis = new BufferedInputStream(fis, 64);
long fileSize = keyfile.length();
if ( fileSize == 0 ) {
throw new KeyFileEmptyException();
} else if ( fileSize == 32 ) {
if ( keyFileData.length == 32 ) {
byte[] outputKey = new byte[32];
if ( bis.read(outputKey, 0, 32) != 32 ) {
if ( bin.read(outputKey, 0, 32) != 32 ) {
throw new IOException("Error reading key.");
}
return outputKey;
} else if ( fileSize == 64 ) {
} else if ( keyFileData.length == 64 ) {
byte[] hex = new byte[64];
bis.mark(64);
if ( bis.read(hex, 0, 64) != 64 ) {
bin.mark(64);
if ( bin.read(hex, 0, 64) != 64 ) {
throw new IOException("Error reading key.");
}
@ -198,7 +196,7 @@ public class PwDatabaseV3 {
return hexStringToByteArray(new String(hex));
} catch (IndexOutOfBoundsException e) {
// Key is not base 64, treat it as binary data
bis.reset();
bin.reset();
}
}
@ -214,7 +212,7 @@ public class PwDatabaseV3 {
try {
while (true) {
int bytesRead = bis.read(buffer, 0, 2048);
bytesRead = bin.read(buffer, 0, 2048);
if ( bytesRead == -1 ) break; // End of file
md.update(buffer, 0, bytesRead);
@ -495,16 +493,16 @@ public class PwDatabaseV3 {
return newId;
}
public byte[] getMasterKey(String key, String keyFileName)
public byte[] getMasterKey(String key, InputStream keyfileStream)
throws InvalidKeyFileException, IOException {
assert (key != null && keyFileName != null);
assert (key != null && keyfileStream != null);
if (key.length() > 0 && keyFileName.length() > 0) {
return getCompositeKey(key, keyFileName);
if (key.length() > 0 && keyfileStream != null) {
return getCompositeKey(key, keyfileStream);
} else if (key.length() > 0) {
return getPasswordKey(key);
} else if (keyFileName.length() > 0) {
return getFileKey(keyFileName);
} else if (keyfileStream != null) {
return getFileKey(keyfileStream);
} else {
throw new IllegalArgumentException("Key cannot be empty.");
}
@ -515,11 +513,6 @@ public class PwDatabaseV3 {
return getPasswordKey(key, "ISO-8859-1");
}
protected byte[] loadXmlKeyFile(String fileName) {
return null;
}
public long getNumRounds() {
return numKeyEncRounds;

View File

@ -123,13 +123,13 @@ public class ImporterV3 {
* @throws InvalidAlgorithmParameterException if error decrypting main file body.
* @throws ShortBufferException if error decrypting main file body.
*/
public PwDatabaseV3 openDatabase( InputStream inStream, String password, String keyfile )
public PwDatabaseV3 openDatabase( InputStream inStream, String password, InputStream keyfileStream )
throws IOException, InvalidDBException
{
return openDatabase(inStream, password, keyfile, new UpdateStatus());
return openDatabase(inStream, password, keyfileStream, new UpdateStatus());
}
public PwDatabaseV3 openDatabase( InputStream inStream, String password, String keyfile, UpdateStatus status )
public PwDatabaseV3 openDatabase( InputStream inStream, String password, InputStream keyfileStream, UpdateStatus status )
throws IOException, InvalidDBException
{
PwDatabaseV3 newManager;
@ -175,7 +175,7 @@ public class ImporterV3 {
}
newManager = createDB();
newManager.setMasterKey( password, keyfile );
newManager.setMasterKey( password, keyfileStream );
// Select algorithm
if( (hdr.flags & PwDbHeaderV3.FLAG_RIJNDAEL) != 0 ) {

View File

@ -327,9 +327,9 @@ namespace keepass2android
defaultPath =>
{
if (defaultPath.StartsWith("sftp://"))
Util.ShowSftpDialog(this, OnReceiveSftpData);
Util.ShowSftpDialog(this, OnReceiveSftpData, () => { });
else
Util.ShowFilenameDialog(this, OnCreateButton, null, false, defaultPath, GetString(Resource.String.enter_filename_details_url),
Util.ShowFilenameDialog(this, OnCreateButton, null, null, false, defaultPath, GetString(Resource.String.enter_filename_details_url),
Intents.RequestCodeFileBrowseForOpen);
}
), true, RequestCodeDbFilename, protocolId);

View File

@ -1,4 +1,4 @@
/*
/*
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin.
Keepass2Android is free software: you can redistribute it and/or modify
@ -682,7 +682,7 @@ namespace keepass2android
addBinaryButton.SetCompoundDrawablesWithIntrinsicBounds( Resources.GetDrawable(Android.Resource.Drawable.IcMenuAdd) , null, null, null);
addBinaryButton.Click += (sender, e) =>
{
Util.ShowBrowseDialog("/mnt/sdcard", this, Intents.RequestCodeFileBrowseForBinary, false);
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false);
};
binariesGroup.AddView(addBinaryButton,layoutParams);

View File

@ -61,9 +61,9 @@ namespace keepass2android
defaultPath =>
{
if (defaultPath.StartsWith("sftp://"))
Util.ShowSftpDialog(this, OnReceiveSftpData);
Util.ShowSftpDialog(this, OnReceiveSftpData, () => { });
else
Util.ShowFilenameDialog(this, OnCreateButton, null, false, defaultPath, GetString(Resource.String.enter_filename_details_url),
Util.ShowFilenameDialog(this, OnCreateButton, null, null, false, defaultPath, GetString(Resource.String.enter_filename_details_url),
Intents.RequestCodeFileBrowseForOpen);
}
), true, RequestCodeDbFilename, protocolId);

View File

@ -14,7 +14,7 @@ namespace keepass2android
[Activity (Label = "@string/app_name", ConfigurationChanges=ConfigChanges.Orientation|ConfigChanges.KeyboardHidden , Theme="@style/NoTitleBar")]
public class FileStorageSelectionActivity : ListActivity
{
private ActivityDesign _design;
private readonly ActivityDesign _design;
private FileStorageAdapter _fileStorageAdapter;
@ -42,6 +42,9 @@ namespace keepass2android
//put file:// to the top
_protocolIds.Remove("file");
_protocolIds.Insert(0, "file");
//remove "content" (covered by androidget)
_protocolIds.Remove("content");
if (context.Intent.GetBooleanExtra(AllowThirdPartyAppGet, false))
_protocolIds.Add("androidget");
if (context.Intent.GetBooleanExtra(AllowThirdPartyAppSend, false))

View File

@ -24,6 +24,7 @@ using System.Xml.Serialization;
using Android.App;
using Android.Content;
using Android.Database;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Views;
@ -86,7 +87,7 @@ namespace keepass2android
private const int RequestCodePrepareDbFile = 1000;
private const int RequestCodePrepareOtpAuxFile = 1001;
private const int RequestCodeChallengeYubikey = 1002;
private const int RequestCodeSelectKeyfile = 1003;
private Task<MemoryStream> _loadDbTask;
private IOConnectionInfo _ioConnection;
@ -137,6 +138,7 @@ namespace keepass2android
private ActivityDesign _design;
private bool _performingLoad;
public PasswordActivity (IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
@ -258,26 +260,20 @@ namespace keepass2android
KcpKeyFile kcpKeyfile = (KcpKeyFile)App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(typeof(KcpKeyFile));
SetEditText(Resource.Id.pass_keyfile, kcpKeyfile.Path);
FindViewById<TextView>(Resource.Id.label_keyfilename).Text =
App.Kp2a.GetFileStorage(kcpKeyfile.Ioc).GetDisplayName(kcpKeyfile.Ioc);
}
}
App.Kp2a.LockDatabase(false);
break;
case Result.Ok: // Key file browse dialog OK'ed.
if (requestCode == Intents.RequestCodeFileBrowseForKeyfile) {
string filename = Util.IntentToFilename(data, this);
if (filename != null) {
if (filename.StartsWith("file://")) {
filename = filename.Substring(7);
}
filename = URLDecoder.Decode(filename);
EditText fn = (EditText) FindViewById(Resource.Id.pass_keyfile);
fn.Text = filename;
}
case Result.Ok:
if (requestCode == RequestCodeSelectKeyfile)
{
IOConnectionInfo ioc = new IOConnectionInfo();
SetIoConnectionFromIntent(ioc, data);
_keyFileOrProvider = IOConnectionInfo.SerializeToString(ioc);
UpdateKeyfileIocView();
}
break;
case (Result)FileStorageResults.FileUsagePrepared:
@ -354,6 +350,39 @@ namespace keepass2android
}
private void UpdateKeyfileIocView()
{
//store keyfile in the view so that we can show the selected keyfile again if the user switches to another key provider and back to key file
FindViewById<TextView>(Resource.Id.label_keyfilename).Tag = _keyFileOrProvider;
if (string.IsNullOrEmpty(_keyFileOrProvider))
{
FindViewById<TextView>(Resource.Id.filestorage_label).Visibility = ViewStates.Gone;
FindViewById<ImageView>(Resource.Id.filestorage_logo).Visibility = ViewStates.Gone;
FindViewById<TextView>(Resource.Id.label_keyfilename).Text = Resources.GetString(Resource.String.no_keyfile_selected);
return;
}
var ioc = IOConnectionInfo.UnserializeFromString(_keyFileOrProvider);
string displayPath = App.Kp2a.GetFileStorage(ioc).GetDisplayName(ioc);
int protocolSeparatorPos = displayPath.IndexOf("://", StringComparison.Ordinal);
string protocolId = protocolSeparatorPos < 0 ?
"file" : displayPath.Substring(0, protocolSeparatorPos);
Drawable drawable = App.Kp2a.GetResourceDrawable("ic_storage_" + protocolId);
FindViewById<ImageView>(Resource.Id.filestorage_logo).SetImageDrawable(drawable);
FindViewById<ImageView>(Resource.Id.filestorage_logo).Visibility = ViewStates.Visible;
String title = App.Kp2a.GetResourceString("filestoragename_" + protocolId);
FindViewById<TextView>(Resource.Id.filestorage_label).Text = title;
FindViewById<TextView>(Resource.Id.filestorage_label).Visibility = ViewStates.Visible;
FindViewById<TextView>(Resource.Id.label_keyfilename).Text = protocolSeparatorPos < 0 ?
displayPath :
displayPath.Substring(protocolSeparatorPos + 3);
}
private void LoadOtpFile()
{
new LoadingDialog<object, object, object>(this, true,
@ -543,14 +572,11 @@ namespace keepass2android
InitializeFilenameView();
if (KeyProviderType == KeyProviders.KeyFile)
SetEditText(Resource.Id.pass_keyfile, _keyFileOrProvider);
{
UpdateKeyfileIocView();
}
FindViewById<EditText>(Resource.Id.pass_keyfile).TextChanged +=
(sender, args) =>
{
_keyFileOrProvider = FindViewById<EditText>(Resource.Id.pass_keyfile).Text;
UpdateOkButtonState();
};
FindViewById<EditText>(Resource.Id.password).TextChanged +=
(sender, args) =>
@ -705,20 +731,14 @@ namespace keepass2android
private void InitializeKeyfileBrowseButton()
{
ImageButton browse = (ImageButton) FindViewById(Resource.Id.browse_button);
browse.Click += (sender, evt) =>
var browseButton = (Button)FindViewById(Resource.Id.btn_change_location);
browseButton.Click += (sender, evt) =>
{
string filename = null;
if (!String.IsNullOrEmpty(_ioConnection.Path))
{
File keyfile = new File(_ioConnection.Path);
File parent = keyfile.ParentFile;
if (parent != null)
{
filename = parent.AbsolutePath;
}
}
Util.ShowBrowseDialog(filename, this, Intents.RequestCodeFileBrowseForKeyfile, false);
Intent intent = new Intent(this, typeof(SelectStorageLocationActivity));
intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, true);
intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, false);
intent.PutExtra(FileStorageSetupDefs.ExtraIsForSave, false);
StartActivityForResult(intent, RequestCodeSelectKeyfile);
};
}
@ -738,7 +758,7 @@ namespace keepass2android
break;
case 1:
//don't set to "" to prevent losing the filename. (ItemSelected is also called during recreation!)
_keyFileOrProvider = FindViewById<EditText>(Resource.Id.pass_keyfile).Text;
_keyFileOrProvider = (FindViewById(Resource.Id.label_keyfilename).Tag ?? "").ToString();
break;
case 2:
_keyFileOrProvider = KeyProviderIdOtp;
@ -779,7 +799,7 @@ namespace keepass2android
_showPassword = savedInstanceState.GetBoolean(ShowpasswordKey, false);
MakePasswordMaskedOrVisible();
_keyFileOrProvider = FindViewById<EditText>(Resource.Id.pass_keyfile).Text = savedInstanceState.GetString(KeyFileOrProviderKey);
_keyFileOrProvider = savedInstanceState.GetString(KeyFileOrProviderKey);
_password = FindViewById<EditText>(Resource.Id.password).Text = savedInstanceState.GetString(PasswordKey);
_pendingOtps = new List<string>(savedInstanceState.GetStringArrayList(PendingOtpsKey));
@ -850,6 +870,11 @@ namespace keepass2android
FindViewById(Resource.Id.keyfileLine).Visibility = KeyProviderType == KeyProviders.KeyFile
? ViewStates.Visible
: ViewStates.Gone;
if (KeyProviderType == KeyProviders.KeyFile)
{
UpdateKeyfileIocView();
}
FindViewById(Resource.Id.otpView).Visibility = KeyProviderType == KeyProviders.Otp
? ViewStates.Visible
: ViewStates.Gone;
@ -880,12 +905,26 @@ namespace keepass2android
{
try
{
compositeKey.AddUserKey(new KcpKeyFile(_keyFileOrProvider));
if (_keyFileOrProvider == "")
throw new System.IO.FileNotFoundException();
var ioc = IOConnectionInfo.UnserializeFromString(_keyFileOrProvider);
using (var stream = App.Kp2a.GetFileStorage(ioc).OpenFileForRead(ioc))
{
byte[] keyfileData = StreamToMemoryStream(stream).ToArray();
compositeKey.AddUserKey(new KcpKeyFile(keyfileData, ioc, true));
}
}
catch (System.IO.FileNotFoundException e)
{
Kp2aLog.Log(e.ToString());
Toast.MakeText(this, App.Kp2a.GetResourceString(UiStringKey.keyfile_does_not_exist), ToastLength.Long).Show();
return;
}
catch (Exception e)
{
Kp2aLog.Log(e.ToString());
Toast.MakeText(this, App.Kp2a.GetResourceString(UiStringKey.keyfile_does_not_exist), ToastLength.Long).Show();
Toast.MakeText(this, e.Message, ToastLength.Long).Show();
return;
}
}
@ -1032,23 +1071,29 @@ namespace keepass2android
var fileStorage = App.Kp2a.GetFileStorage(_ioConnection);
var stream = fileStorage.OpenFileForRead(_ioConnection);
var memoryStream = StreamToMemoryStream(stream);
Kp2aLog.Log("Pre-loading database file completed");
return memoryStream;
}
private static MemoryStream StreamToMemoryStream(Stream stream)
{
var memoryStream = stream as MemoryStream;
if (memoryStream == null)
{
// Read the file into memory
// Read the stream into memory
int capacity = 4096; // Default initial capacity, if stream can't report it.
if (stream.CanSeek)
{
capacity = (int)stream.Length;
capacity = (int) stream.Length;
}
memoryStream = new MemoryStream(capacity);
stream.CopyTo(memoryStream);
stream.Close();
memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
memoryStream.Seek(0, SeekOrigin.Begin);
}
Kp2aLog.Log("Pre-loading database file completed");
return memoryStream;
}

File diff suppressed because it is too large Load Diff

View File

@ -86,25 +86,61 @@ android:layout_height="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/ic_menu_view" />
</LinearLayout>
<LinearLayout
android:id="@+id/keyfileLine"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:orientation="horizontal">
<EditText
android:id="@+id/pass_keyfile"
android:layout_width="0px"
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_weight="1"
android:layout_gravity="bottom"
android:hint="@string/entry_keyfile" />
<ImageButton
android:id="@+id/browse_button"
android:orientation="vertical">
<TextView
android:id="@+id/keyfile_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/keyfile_heading" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/filestorage_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_folder_small" />
android:src="@drawable/ic_storage_file"
android:padding="5dp"
/>
<TextView
android:id="@+id/filestorage_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Local file (TODO!)"
android:textSize="16dp" >
</TextView>
</LinearLayout>
<TextView
android:id="@+id/label_keyfilename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="[path]"
android:layout_marginLeft="18dp"
/>
<Button android:id="@+id/btn_change_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button_change_location"
style="@style/TextAppearance_SubElement"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/otpView"

View File

@ -32,6 +32,8 @@
<string name="ShowGroupInEntry_title">Show group name in entry view</string>
<string name="unknown_uri_scheme">Sorry! Keepass2Android cannot handle the returned URI %1$s. Please contact the developer!</string>
<string name="security_prefs">Security</string>
<string name="display_prefs">Display</string>
<string name="password_access_prefs">Password entry access</string>
@ -68,6 +70,7 @@
<string name="entry_expires">Expires</string>
<string name="entry_group_name">Group Name</string>
<string name="entry_keyfile">Key file (optional)</string>
<string name="keyfile_heading">Key file</string>
<string name="entry_modified">Modified</string>
<string name="entry_password">Password</string>
<string name="entry_save">Save</string>
@ -114,6 +117,7 @@
<string name="invalid_algorithm">Invalid algorithm.</string>
<string name="invalid_db_sig">Database format not recognized.</string>
<string name="keyfile_does_not_exist">Key file does not exist.</string>
<string name="no_keyfile_selected">No key file selected.</string>
<string name="keyfile_is_empty">Key file is empty.</string>
<string name="length">Length</string>
<string name="list_size_title">Group list size</string>

View File

@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using KeePassLib.Serialization;
using keepass2android.Io;
using Environment = Android.OS.Environment;
namespace keepass2android
{
[Activity(Label = "")]
public class SelectStorageLocationActivity : Activity
{
private ActivityDesign _design;
private bool _isRecreated;
private const int RequestCodeFileStorageSelection = 983713;
public SelectStorageLocationActivity()
{
_design = new ActivityDesign(this);
}
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
_design.ApplyTheme();
Kp2aLog.Log("SelectStorageLocationActivity.OnCreate");
IsForSave = Intent.GetBooleanExtra(FileStorageSetupDefs.ExtraIsForSave, false);
if (IsForSave)
{
throw new Exception("save is not yet implemented. In StartSelectFile, no handler for onCreate is passed.");
}
bool allowThirdPartyGet = Intent.GetBooleanExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, false);
bool allowThirdPartySend = Intent.GetBooleanExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, false);
if (bundle == null)
State = new Bundle();
else
{
State = (Bundle)bundle.Clone();
_isRecreated = true;
}
if (!_isRecreated)
{
Intent intent = new Intent(this, typeof(FileStorageSelectionActivity));
intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, allowThirdPartyGet);
intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, allowThirdPartySend);
StartActivityForResult(intent, RequestCodeFileStorageSelection);
}
}
protected Bundle State { get; set; }
protected bool IsForSave { get; set; }
protected override void OnSaveInstanceState(Bundle outState)
{
base.OnSaveInstanceState(outState);
outState.PutAll(State);
}
protected override void OnResume()
{
base.OnResume();
_design.ReapplyTheme();
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == RequestCodeFileStorageSelection)
{
if (resultCode == KeePass.ExitFileStorageSelectionOk)
{
string protocolId = data.GetStringExtra("protocolId");
if (protocolId == "androidget")
{
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForOpen, false);
}
else
{
App.Kp2a.GetFileStorage(protocolId).StartSelectFile(new FileStorageSetupInitiatorActivity(this,
OnActivityResult,
defaultPath =>
{
if (defaultPath.StartsWith("sftp://"))
Util.ShowSftpDialog(this, OnReceivedSftpData, ReturnCancel);
else
Util.ShowFilenameDialog(this, OnOpenButton, null, ReturnCancel, false, defaultPath, GetString(Resource.String.enter_filename_details_url),
Intents.RequestCodeFileBrowseForOpen);
}
), false, 0, protocolId);
}
}
else
{
if (resultCode == (Result)FileStorageResults.FileChooserPrepared)
{
IOConnectionInfo ioc = new IOConnectionInfo();
PasswordActivity.SetIoConnectionFromIntent(ioc, data);
#if !EXCLUDE_FILECHOOSER
StartFileChooser(ioc.Path);
#else
ReturnIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/yubi.kdbx" });
#endif
return;
}
if ((resultCode == Result.Canceled) && (data != null) && (data.HasExtra("EXTRA_ERROR_MESSAGE")))
{
Toast.MakeText(this, data.GetStringExtra("EXTRA_ERROR_MESSAGE"), ToastLength.Long).Show();
}
ReturnCancel();
}
}
if (requestCode == Intents.RequestCodeFileBrowseForOpen)
{
if (resultCode == Result.Ok)
{
string filename = Util.IntentToFilename(data, this);
if (filename != null)
{
if (filename.StartsWith("file://"))
{
filename = filename.Substring(7);
filename = Java.Net.URLDecoder.Decode(filename);
}
IOConnectionInfo ioc = new IOConnectionInfo
{
Path = filename
};
ReturnIoc(ioc);
}
else
{
if (data.Data.Scheme == "content")
{
ReturnIoc(IOConnectionInfo.FromPath(data.DataString));
}
else
{
Toast.MakeText(this, Resources.GetString(Resource.String.unknown_uri_scheme, new Java.Lang.Object[] {data.DataString}),
ToastLength.Long).Show();
ReturnCancel();
}
}
}
else
{
ReturnCancel();
}
}
}
private void ReturnCancel()
{
SetResult(Result.Canceled);
Finish();
}
private void ReturnIoc(IOConnectionInfo ioc)
{
Intent intent = new Intent();
PasswordActivity.PutIoConnectionToIntent(ioc, intent);
SetResult(Result.Ok, intent);
Finish();
}
private bool OnReceivedSftpData(string filename)
{
IOConnectionInfo ioc = new IOConnectionInfo { Path = filename };
#if !EXCLUDE_FILECHOOSER
StartFileChooser(ioc.Path);
#else
ReturnIoc(ioc);
#endif
return true;
}
#if !EXCLUDE_FILECHOOSER
private void StartFileChooser(string defaultPath)
{
Kp2aLog.Log("FSA: defaultPath="+defaultPath);
string fileProviderAuthority = FileChooserFileProvider.TheAuthority;
if (defaultPath.StartsWith("file://"))
{
fileProviderAuthority = PackageName+".android-filechooser.localfile";
}
Intent i = Keepass2android.Kp2afilechooser.Kp2aFileChooserBridge.GetLaunchFileChooserIntent(this, fileProviderAuthority,
defaultPath);
StartActivityForResult(i, Intents.RequestCodeFileBrowseForOpen);
}
#endif
private bool OnOpenButton(String fileName)
{
IOConnectionInfo ioc = new IOConnectionInfo
{
Path = fileName
};
ReturnIoc(ioc);
return true;
}
}
}

View File

@ -141,7 +141,7 @@ namespace keepass2android
return list.Count > 0;
}
public static void ShowBrowseDialog(string filename, Activity act, int requestCodeBrowse, bool forSaving)
public static void ShowBrowseDialog(Activity act, int requestCodeBrowse, bool forSaving)
{
if ((!forSaving) && (IsIntentAvailable(act, Intent.ActionGetContent, "*/*", new List<string> { Intent.CategoryOpenable})))
{
@ -223,7 +223,7 @@ namespace keepass2android
public delegate bool FileSelectedHandler(string filename);
public static void ShowSftpDialog(Activity activity, FileSelectedHandler onStartBrowse)
public static void ShowSftpDialog(Activity activity, FileSelectedHandler onStartBrowse, Action onCancel)
{
#if !EXCLUDE_JAVAFILESTORAGE
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
@ -244,7 +244,7 @@ namespace keepass2android
password);
onStartBrowse(sftpPath);
});
builder.SetNegativeButton(Android.Resource.String.Cancel, (sender, args) => {});
builder.SetNegativeButton(Android.Resource.String.Cancel, onCancel);
builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title));
Dialog dialog = builder.Create();
@ -252,8 +252,22 @@ namespace keepass2android
#endif
}
public static void ShowFilenameDialog(Activity activity, FileSelectedHandler onOpen, FileSelectedHandler onCreate, bool showBrowseButton,
string defaultFilename, string detailsText, int requestCodeBrowse)
class DismissListener: Java.Lang.Object, IDialogInterfaceOnDismissListener
{
private readonly Action _onDismiss;
public DismissListener(Action onDismiss)
{
_onDismiss = onDismiss;
}
public void OnDismiss(IDialogInterface dialog)
{
_onDismiss();
}
}
public static void ShowFilenameDialog(Activity activity, FileSelectedHandler onOpen, FileSelectedHandler onCreate, Action onCancel, bool showBrowseButton, string defaultFilename, string detailsText, int requestCodeBrowse)
{
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.SetView(activity.LayoutInflater.Inflate(Resource.Layout.file_selection_filename, null));
@ -262,6 +276,7 @@ namespace keepass2android
Button openButton = (Button) dialog.FindViewById(Resource.Id.open);
Button createButton = (Button) dialog.FindViewById(Resource.Id.create);
TextView enterFilenameDetails = (TextView) dialog.FindViewById(Resource.Id.label_open_by_filename_details);
openButton.Visibility = onOpen != null ? ViewStates.Visible : ViewStates.Gone;
createButton.Visibility = onCreate != null? ViewStates.Visible : ViewStates.Gone;
@ -290,7 +305,15 @@ namespace keepass2android
};
Button cancelButton = (Button) dialog.FindViewById(Resource.Id.fnv_cancel);
cancelButton.Click += (sender, e) => dialog.Dismiss();
cancelButton.Click += delegate
{
dialog.Dismiss();
};
if (onCancel != null)
dialog.SetOnDismissListener(new DismissListener(onCancel));
ImageButton browseButton = (ImageButton) dialog.FindViewById(Resource.Id.browse_button);
if (!showBrowseButton)
@ -301,7 +324,7 @@ namespace keepass2android
{
string filename = ((EditText) dialog.FindViewById(Resource.Id.file_filename)).Text;
Util.ShowBrowseDialog(filename, activity, requestCodeBrowse, onCreate != null);
Util.ShowBrowseDialog(activity, requestCodeBrowse, onCreate != null);
};

View File

@ -443,7 +443,8 @@ namespace keepass2android
new SftpFileStorage(this),
#endif
#endif
new BuiltInFileStorage(this)
new BuiltInFileStorage(this),
new AndroidContentStorage(Application.Context)
};
}
return _fileStorages;

View File

@ -69,6 +69,7 @@ namespace keepass2android
view.FileSelectButtons _fileSelectButtons;
internal AppTask AppTask;
private const int RequestCodeSelectIoc = 456;
public const string NoForwardToPasswordActivity = "NoForwardToPasswordActivity";
@ -129,9 +130,11 @@ namespace keepass2android
EventHandler openFileButtonClick = (sender, e) =>
{
Intent intent = new Intent(this, typeof(FileStorageSelectionActivity));
Intent intent = new Intent(this, typeof(SelectStorageLocationActivity));
intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, true);
StartActivityForResult(intent, 0);
intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, false);
intent.PutExtra(FileStorageSetupDefs.ExtraIsForSave, false);
StartActivityForResult(intent, RequestCodeSelectIoc);
};
openFileButton.Click += openFileButtonClick;
@ -294,19 +297,7 @@ namespace keepass2android
App.Kp2a.GetFileStorage(ioc)
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), ioc, 0, false);
}
private bool OnOpenButton(String fileName)
{
IOConnectionInfo ioc = new IOConnectionInfo
{
Path = fileName
};
LaunchPasswordActivityForIoc(ioc);
return true;
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
@ -326,109 +317,23 @@ namespace keepass2android
FillData();
if (resultCode == KeePass.ExitFileStorageSelectionOk)
{
string protocolId = data.GetStringExtra("protocolId");
if (protocolId == "androidget")
{
string defaultFilename = Environment.ExternalStorageDirectory +
GetString(Resource.String.default_file_path);
Util.ShowBrowseDialog(defaultFilename, this, Intents.RequestCodeFileBrowseForOpen, false);
}
else
{
App.Kp2a.GetFileStorage(protocolId).StartSelectFile(new FileStorageSetupInitiatorActivity(this,
OnActivityResult,
defaultPath =>
{
if (defaultPath.StartsWith("sftp://"))
Util.ShowSftpDialog(this, OnReceivedSftpData);
else
Util.ShowFilenameDialog(this, OnOpenButton, null, false, defaultPath, GetString(Resource.String.enter_filename_details_url),
Intents.RequestCodeFileBrowseForOpen);
}
), false, 0, protocolId);
}
}
if ( (requestCode == Intents.RequestCodeFileBrowseForCreate
|| requestCode == Intents.RequestCodeFileBrowseForOpen)
&& resultCode == Result.Ok) {
string filename = Util.IntentToFilename(data, this);
if (filename != null) {
if (filename.StartsWith("file://")) {
filename = filename.Substring(7);
filename = Java.Net.URLDecoder.Decode(filename);
}
if (requestCode == Intents.RequestCodeFileBrowseForOpen)
{
IOConnectionInfo ioc = new IOConnectionInfo
{
Path = filename
};
LaunchPasswordActivityForIoc(ioc);
}
}
}
if (resultCode == (Result) FileStorageResults.FileUsagePrepared)
if (resultCode == (Result)FileStorageResults.FileUsagePrepared)
{
IOConnectionInfo ioc = new IOConnectionInfo();
PasswordActivity.SetIoConnectionFromIntent(ioc, data);
LaunchPasswordActivityForIoc(ioc);
}
if (resultCode == (Result)FileStorageResults.FileChooserPrepared)
if ((resultCode == Result.Ok) && (requestCode == RequestCodeSelectIoc))
{
IOConnectionInfo ioc = new IOConnectionInfo();
PasswordActivity.SetIoConnectionFromIntent(ioc, data);
#if !EXCLUDE_FILECHOOSER
StartFileChooser(ioc.Path);
#else
LaunchPasswordActivityForIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/yubi.kdbx"});
#endif
}
if ((resultCode == Result.Canceled) && (data != null) && (data.HasExtra("EXTRA_ERROR_MESSAGE")))
{
Toast.MakeText(this, data.GetStringExtra("EXTRA_ERROR_MESSAGE"), ToastLength.Long).Show();
LaunchPasswordActivityForIoc(ioc);
}
}
private bool OnReceivedSftpData(string filename)
{
IOConnectionInfo ioc = new IOConnectionInfo { Path = filename };
#if !EXCLUDE_FILECHOOSER
StartFileChooser(ioc.Path);
#else
LaunchPasswordActivityForIoc(ioc);
#endif
return true;
}
#if !EXCLUDE_FILECHOOSER
private void StartFileChooser(string defaultPath)
{
Kp2aLog.Log("FSA: defaultPath="+defaultPath);
string fileProviderAuthority = FileChooserFileProvider.TheAuthority;
if (defaultPath.StartsWith("file://"))
{
fileProviderAuthority = PackageName+".android-filechooser.localfile";
}
Intent i = Keepass2android.Kp2afilechooser.Kp2aFileChooserBridge.GetLaunchFileChooserIntent(this, fileProviderAuthority,
defaultPath);
StartActivityForResult(i, Intents.RequestCodeFileBrowseForOpen);
}
#endif
protected override void OnResume()
{
base.OnResume();

View File

@ -58,6 +58,12 @@ namespace keepass2android.fileselect
}
protected override void OnRestart()
{
base.OnRestart();
_isRecreated = true;
}
protected override void OnStart()
{
base.OnStart();

View File

@ -30,7 +30,7 @@
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;EXCLUDE_TWOFISH;INCLUDE_KEYBOARD;INCLUDE_FILECHOOSER;INCLUDE_JAVAFILESTORAGE;EXCLUDE_KEYTRANSFORM</DefineConstants>
<DefineConstants>DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>False</ConsolePause>
@ -85,9 +85,6 @@
<Reference Include="System.Core" />
<Reference Include="Mono.Android" />
<Reference Include="Mono.Android.Support.v4" />
<Reference Include="GooglePlayServicesFroyoLib">
<HintPath>..\Components\googleplayservicesfroyo-9.0\lib\android\GooglePlayServicesFroyoLib.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="addons\OtpKeyProv\EncodingUtil.cs" />
@ -138,6 +135,7 @@
<Compile Include="fileselect\FileSelectActivity.cs" />
<Compile Include="fileselect\FileDbHelper.cs" />
<Compile Include="search\SearchProvider.cs" />
<Compile Include="SelectStorageLocationActivity.cs" />
<Compile Include="services\OngoingNotificationsService.cs" />
<Compile Include="settings\DatabaseSettingsActivity.cs" />
<Compile Include="intents\Intents.cs" />
@ -832,12 +830,6 @@
<ItemGroup>
<AndroidResource Include="Resources\layout\text_with_help.xml" />
</ItemGroup>
<ItemGroup>
<XamarinComponentReference Include="googleplayservicesfroyo">
<Version>9.0</Version>
<Visible>False</Visible>
</XamarinComponentReference>
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\ic_storage_skydrive.png" />
</ItemGroup>