mirror of
https://github.com/bitwarden/mobile
synced 2025-12-29 22:53:34 +00:00
initial commit
This commit is contained in:
9
src/App/Abstractions/IDataObject.cs
Normal file
9
src/App/Abstractions/IDataObject.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IDataObject<T> where T : IEquatable<T>
|
||||
{
|
||||
T Id { get; }
|
||||
}
|
||||
}
|
||||
13
src/App/Abstractions/Services/IApiService.cs
Normal file
13
src/App/Abstractions/Services/IApiService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models.Api;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IApiService
|
||||
{
|
||||
HttpClient Client { get; set; }
|
||||
|
||||
Task<ApiResult<T>> HandleErrorAsync<T>(HttpResponseMessage response);
|
||||
}
|
||||
}
|
||||
13
src/App/Abstractions/Services/IAuthService.cs
Normal file
13
src/App/Abstractions/Services/IAuthService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models.Api;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IAuthService
|
||||
{
|
||||
bool IsAuthenticated { get; }
|
||||
string Token { get; set; }
|
||||
|
||||
Task<ApiResult<TokenResponse>> TokenPostAsync(TokenRequest request);
|
||||
}
|
||||
}
|
||||
17
src/App/Abstractions/Services/ICryptoService.cs
Normal file
17
src/App/Abstractions/Services/ICryptoService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Bit.App.Models;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface ICryptoService
|
||||
{
|
||||
string Base64Key { get; }
|
||||
byte[] Key { get; set; }
|
||||
|
||||
string Decrypt(CipherString encyptedValue);
|
||||
CipherString Encrypt(string plaintextValue);
|
||||
byte[] MakeKeyFromPassword(string password, string salt);
|
||||
string MakeKeyFromPasswordBase64(string password, string salt);
|
||||
byte[] HashPassword(byte[] key, string password);
|
||||
string HashPasswordBase64(byte[] key, string password);
|
||||
}
|
||||
}
|
||||
7
src/App/Abstractions/Services/IDatabaseService.cs
Normal file
7
src/App/Abstractions/Services/IDatabaseService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IDatabaseService
|
||||
{
|
||||
void CreateTables();
|
||||
}
|
||||
}
|
||||
12
src/App/Abstractions/Services/IFolderService.cs
Normal file
12
src/App/Abstractions/Services/IFolderService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IFolderService
|
||||
{
|
||||
Task<IEnumerable<Folder>> GetAllAsync();
|
||||
Task SaveAsync(Folder folder);
|
||||
}
|
||||
}
|
||||
10
src/App/Abstractions/Services/ISecureStorageService.cs
Normal file
10
src/App/Abstractions/Services/ISecureStorageService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface ISecureStorageService
|
||||
{
|
||||
void Store(string key, byte[] dataBytes);
|
||||
byte[] Retrieve(string key);
|
||||
void Delete(string key);
|
||||
bool Contains(string key);
|
||||
}
|
||||
}
|
||||
12
src/App/Abstractions/Services/ISiteService.cs
Normal file
12
src/App/Abstractions/Services/ISiteService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface ISiteService
|
||||
{
|
||||
Task<IEnumerable<Site>> GetAllAsync();
|
||||
Task SaveAsync(Site site);
|
||||
}
|
||||
}
|
||||
9
src/App/Abstractions/Services/ISqlService.cs
Normal file
9
src/App/Abstractions/Services/ISqlService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using SQLite;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface ISqlService
|
||||
{
|
||||
SQLiteConnection GetConnection();
|
||||
}
|
||||
}
|
||||
55
src/App/App.cs
Normal file
55
src/App/App.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Views;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
private readonly IDatabaseService _databaseService;
|
||||
|
||||
public App(IAuthService authService, IDatabaseService databaseService)
|
||||
{
|
||||
_databaseService = databaseService;
|
||||
|
||||
if(authService.IsAuthenticated)
|
||||
{
|
||||
var nav = new NavigationPage(new VaultListPage());
|
||||
nav.BarBackgroundColor = Color.FromHex("3c8dbc");
|
||||
nav.BarTextColor = Color.FromHex("ffffff");
|
||||
|
||||
MainPage = nav;
|
||||
}
|
||||
else
|
||||
{
|
||||
var nav = new NavigationPage(new LoginPage());
|
||||
nav.BarBackgroundColor = Color.FromHex("3c8dbc");
|
||||
nav.BarTextColor = Color.FromHex("ffffff");
|
||||
|
||||
MainPage = nav;
|
||||
}
|
||||
|
||||
MainPage.BackgroundColor = Color.FromHex("ecf0f5");
|
||||
}
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
// Handle when your app starts
|
||||
_databaseService.CreateTables();
|
||||
}
|
||||
|
||||
protected override void OnSleep()
|
||||
{
|
||||
// Handle when your app sleeps
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
// Handle when your app resumes
|
||||
}
|
||||
}
|
||||
}
|
||||
149
src/App/App.csproj
Normal file
149
src/App/App.csproj
Normal file
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<MinimumVisualStudioVersion>10.0</MinimumVisualStudioVersion>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{B490C5DA-639E-4994-ABD2-54222B8A348E}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Bit.App</RootNamespace>
|
||||
<AssemblyName>Bit.App</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<TargetFrameworkProfile>Profile111</TargetFrameworkProfile>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>TRACE;DEBUG;NETFX_CORE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Abstractions\Services\ISiteService.cs" />
|
||||
<Compile Include="Abstractions\Services\IFolderService.cs" />
|
||||
<Compile Include="App.cs" />
|
||||
<Compile Include="Abstractions\Services\ISecureStorageService.cs" />
|
||||
<Compile Include="Abstractions\Services\ISqlService.cs" />
|
||||
<Compile Include="Behaviors\EmailValidationBehavior.cs" />
|
||||
<Compile Include="Behaviors\RequiredValidationBehavior.cs" />
|
||||
<Compile Include="Models\Api\ApiError.cs" />
|
||||
<Compile Include="Models\Api\ApiResult.cs" />
|
||||
<Compile Include="Models\Api\Request\FolderRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\SiteRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\TokenRequest.cs" />
|
||||
<Compile Include="Models\Api\Response\ErrorResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\FolderResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\SiteResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\TokenResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\ProfileResponse.cs" />
|
||||
<Compile Include="Models\Cipher.cs" />
|
||||
<Compile Include="Models\CipherString.cs" />
|
||||
<Compile Include="Models\Data\FolderData.cs" />
|
||||
<Compile Include="Abstractions\IDataObject.cs" />
|
||||
<Compile Include="Models\Data\SiteData.cs" />
|
||||
<Compile Include="Models\Folder.cs" />
|
||||
<Compile Include="Models\Site.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\DatabaseService.cs" />
|
||||
<Compile Include="Services\FolderService.cs" />
|
||||
<Compile Include="Services\Repository.cs" />
|
||||
<Compile Include="Abstractions\Services\IApiService.cs" />
|
||||
<Compile Include="Abstractions\Services\IAuthService.cs" />
|
||||
<Compile Include="Abstractions\Services\ICryptoService.cs" />
|
||||
<Compile Include="Abstractions\Services\IDatabaseService.cs" />
|
||||
<Compile Include="Services\SiteService.cs" />
|
||||
<Compile Include="Services\AuthService.cs" />
|
||||
<Compile Include="Services\CryptoService.cs" />
|
||||
<Compile Include="Models\View\VaultView.cs" />
|
||||
<Compile Include="Pages\LoginPage.cs" />
|
||||
<Compile Include="Pages\VaultEditFolderPage.cs" />
|
||||
<Compile Include="Pages\VaultAddFolderPage.cs" />
|
||||
<Compile Include="Pages\VaultAddSitePage.cs" />
|
||||
<Compile Include="Pages\VaultViewSitePage.cs" />
|
||||
<Compile Include="Pages\VaultEditSitePage.cs" />
|
||||
<Compile Include="Pages\VaultListPage.cs" />
|
||||
<Compile Include="Services\ApiService.cs" />
|
||||
<Compile Include="Utilities\Extentions.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="crypto, Version=1.8.1.0, Culture=neutral, PublicKeyToken=0e99375e54769942, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Portable.BouncyCastle.1.8.1\lib\portable-net45+win8+wpa81+MonoTouch10+MonoAndroid10+xamarinmac20+xamarinios10\crypto.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Practices.ServiceLocation, Version=1.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Practices.Unity, Version=3.5.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Unity.3.5.1405-prerelease\lib\portable-net45+wp80+win8+wpa81+MonoAndroid10+MonoTouch10\Microsoft.Practices.Unity.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="ModernHttpClient, Version=2.4.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\modernhttpclient.2.4.2\lib\Portable-Net45+WinRT45+WP8+WPA81\ModernHttpClient.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Newtonsoft.Json.8.0.3\lib\portable-net40+sl5+wp80+win8+wpa81\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="SQLite-net, Version=1.1.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\sqlite-net-pcl.1.1.1\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLite-net.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="SQLitePCL.raw, Version=0.8.5.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\SQLitePCL.raw.0.8.6\lib\portable-net45+netcore45+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCL.raw.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Core, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.2.0.31\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Platform, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.2.0.31\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Platform.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Xamarin.Forms.Xaml, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Xamarin.Forms.2.2.0.31\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Xaml.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="XLabs.Ioc, Version=2.0.5782.12218, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\XLabs.IoC.2.0.5782\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1+Xamarin.iOS10\XLabs.Ioc.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
|
||||
<Import Project="..\..\packages\Xamarin.Forms.2.2.0.31\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.targets" Condition="Exists('..\..\packages\Xamarin.Forms.2.2.0.31\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Forms.2.2.0.31\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Forms.2.2.0.31\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.targets'))" />
|
||||
</Target>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
37
src/App/Behaviors/EmailValidationBehavior.cs
Normal file
37
src/App/Behaviors/EmailValidationBehavior.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Behaviors
|
||||
{
|
||||
public class EmailValidationBehavior : Behavior<Entry>
|
||||
{
|
||||
private const string EmailRegex = @"^(?("")("".+?(?<!\\)""@)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])@))" +
|
||||
@"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-\w]*[0-9a-z]*\.)+[a-z0-9][\-a-z0-9]{0,22}[a-z0-9]))$";
|
||||
|
||||
private static readonly BindablePropertyKey IsValidPropertyKey = BindableProperty.CreateReadOnly("IsValid", typeof(bool), typeof(EmailValidationBehavior), false);
|
||||
public static readonly BindableProperty IsValidProperty = IsValidPropertyKey.BindableProperty;
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get { return (bool)GetValue(IsValidProperty); }
|
||||
private set { SetValue(IsValidPropertyKey, value); }
|
||||
}
|
||||
|
||||
protected override void OnAttachedTo(Entry bindable)
|
||||
{
|
||||
bindable.TextChanged += HandleTextChanged;
|
||||
}
|
||||
|
||||
void HandleTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
IsValid = Regex.IsMatch(e.NewTextValue, EmailRegex, RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250));
|
||||
((Entry)sender).BackgroundColor = IsValid ? Color.Default : Color.Red;
|
||||
}
|
||||
|
||||
protected override void OnDetachingFrom(Entry bindable)
|
||||
{
|
||||
bindable.TextChanged -= HandleTextChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/App/Behaviors/RequiredValidationBehavior.cs
Normal file
33
src/App/Behaviors/RequiredValidationBehavior.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Behaviors
|
||||
{
|
||||
public class RequiredValidationBehavior : Behavior<Entry>
|
||||
{
|
||||
private static readonly BindablePropertyKey IsValidPropertyKey = BindableProperty.CreateReadOnly("IsValid", typeof(bool), typeof(RequiredValidationBehavior), false);
|
||||
public static readonly BindableProperty IsValidProperty = IsValidPropertyKey.BindableProperty;
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get { return (bool)GetValue(IsValidProperty); }
|
||||
private set { SetValue(IsValidPropertyKey, value); }
|
||||
}
|
||||
|
||||
protected override void OnAttachedTo(Entry bindable)
|
||||
{
|
||||
bindable.TextChanged += HandleTextChanged;
|
||||
}
|
||||
|
||||
void HandleTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
IsValid = !string.IsNullOrWhiteSpace(e.NewTextValue);
|
||||
((Entry)sender).BackgroundColor = IsValid ? Color.Default : Color.Red;
|
||||
}
|
||||
|
||||
protected override void OnDetachingFrom(Entry bindable)
|
||||
{
|
||||
bindable.TextChanged -= HandleTextChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/App/Models/Api/ApiError.cs
Normal file
10
src/App/Models/Api/ApiError.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class ApiError
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public HttpStatusCode StatusCode { get; set; }
|
||||
}
|
||||
}
|
||||
33
src/App/Models/Api/ApiResult.cs
Normal file
33
src/App/Models/Api/ApiResult.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class ApiResult<T>
|
||||
{
|
||||
private List<ApiError> m_errors = new List<ApiError>();
|
||||
|
||||
public bool Succeeded { get; private set; }
|
||||
public T Result { get; set; }
|
||||
public IEnumerable<ApiError> Errors => m_errors;
|
||||
|
||||
public static ApiResult<T> Success(T result)
|
||||
{
|
||||
return new ApiResult<T>
|
||||
{
|
||||
Succeeded = true,
|
||||
Result = result
|
||||
};
|
||||
}
|
||||
|
||||
public static ApiResult<T> Failed(params ApiError[] errors)
|
||||
{
|
||||
var result = new ApiResult<T> { Succeeded = false };
|
||||
if(errors != null)
|
||||
{
|
||||
result.m_errors.AddRange(errors);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/App/Models/Api/Request/FolderRequest.cs
Normal file
7
src/App/Models/Api/Request/FolderRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class FolderRequest
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
12
src/App/Models/Api/Request/SiteRequest.cs
Normal file
12
src/App/Models/Api/Request/SiteRequest.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class SiteRequest
|
||||
{
|
||||
public string FolderId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
}
|
||||
8
src/App/Models/Api/Request/TokenRequest.cs
Normal file
8
src/App/Models/Api/Request/TokenRequest.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class TokenRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
||||
14
src/App/Models/Api/Response/ErrorResponse.cs
Normal file
14
src/App/Models/Api/Response/ErrorResponse.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class ErrorResponse
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public Dictionary<string, IEnumerable<string>> ValidationErrors { get; set; }
|
||||
// For use in development environments.
|
||||
public string ExceptionMessage { get; set; }
|
||||
public string ExceptionStackTrace { get; set; }
|
||||
public string InnerExceptionMessage { get; set; }
|
||||
}
|
||||
}
|
||||
8
src/App/Models/Api/Response/FolderResponse.cs
Normal file
8
src/App/Models/Api/Response/FolderResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class FolderResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
12
src/App/Models/Api/Response/ProfileResponse.cs
Normal file
12
src/App/Models/Api/Response/ProfileResponse.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class ProfileResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public string Culture { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
16
src/App/Models/Api/Response/SiteResponse.cs
Normal file
16
src/App/Models/Api/Response/SiteResponse.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class SiteResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Notes { get; set; }
|
||||
|
||||
// Expandables
|
||||
public FolderResponse Folder { get; set; }
|
||||
}
|
||||
}
|
||||
8
src/App/Models/Api/Response/TokenResponse.cs
Normal file
8
src/App/Models/Api/Response/TokenResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class TokenResponse
|
||||
{
|
||||
public string Token { get; set; }
|
||||
public ProfileResponse Profile { get; set; }
|
||||
}
|
||||
}
|
||||
11
src/App/Models/Cipher.cs
Normal file
11
src/App/Models/Cipher.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public abstract class Cipher
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string ServerId { get; set; }
|
||||
public CipherString Name { get; set; }
|
||||
}
|
||||
}
|
||||
50
src/App/Models/CipherString.cs
Normal file
50
src/App/Models/CipherString.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using XLabs.Ioc;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class CipherString
|
||||
{
|
||||
public CipherString(string encryptedString)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(encryptedString) || !encryptedString.Contains("|"))
|
||||
{
|
||||
throw new ArgumentException(nameof(encryptedString));
|
||||
}
|
||||
|
||||
EncryptedString = encryptedString;
|
||||
}
|
||||
|
||||
public CipherString(string initializationVector, string cipherText)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(initializationVector))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(initializationVector));
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(cipherText))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cipherText));
|
||||
}
|
||||
|
||||
EncryptedString = string.Format("{0}|{1}", initializationVector, cipherText);
|
||||
}
|
||||
|
||||
public string EncryptedString { get; private set; }
|
||||
public string InitializationVector { get { return EncryptedString?.Split('|')[0]; } }
|
||||
public string CipherText { get { return EncryptedString?.Split('|')[1]; } }
|
||||
public byte[] InitializationVectorBytes { get { return Convert.FromBase64String(InitializationVector); } }
|
||||
public byte[] CipherTextBytes { get { return Convert.FromBase64String(CipherText); } }
|
||||
|
||||
public string Decrypt()
|
||||
{
|
||||
var cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
return cryptoService.Decrypt(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/App/Models/Data/FolderData.cs
Normal file
32
src/App/Models/Data/FolderData.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Bit.App.Abstractions;
|
||||
|
||||
namespace Bit.App.Models.Data
|
||||
{
|
||||
[Table("Folder")]
|
||||
public class FolderData : IDataObject<int>
|
||||
{
|
||||
public FolderData()
|
||||
{ }
|
||||
|
||||
public FolderData(Folder folder)
|
||||
{
|
||||
Id = folder.Id;
|
||||
ServerId = folder.ServerId;
|
||||
Name = folder.Name?.EncryptedString;
|
||||
}
|
||||
|
||||
[PrimaryKey]
|
||||
[AutoIncrement]
|
||||
public int Id { get; set; }
|
||||
public string ServerId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public Folder ToFolder()
|
||||
{
|
||||
return new Folder(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/App/Models/Data/SiteData.cs
Normal file
44
src/App/Models/Data/SiteData.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Bit.App.Abstractions;
|
||||
|
||||
namespace Bit.App.Models.Data
|
||||
{
|
||||
[Table("Site")]
|
||||
public class SiteData : IDataObject<int>
|
||||
{
|
||||
public SiteData()
|
||||
{ }
|
||||
|
||||
public SiteData(Site site)
|
||||
{
|
||||
Id = site.Id;
|
||||
ServerId = site.ServerId;
|
||||
FolderId = site.FolderId;
|
||||
ServerFolderId = site.ServerFolderId;
|
||||
Name = site.Name?.EncryptedString;
|
||||
Uri = site.Uri?.EncryptedString;
|
||||
Username = site.Username?.EncryptedString;
|
||||
Password = site.Password?.EncryptedString;
|
||||
Notes = site.Notes?.EncryptedString;
|
||||
}
|
||||
|
||||
[PrimaryKey]
|
||||
[AutoIncrement]
|
||||
public int Id { get; set; }
|
||||
public string ServerId { get; set; }
|
||||
public int? FolderId { get; set; }
|
||||
public string ServerFolderId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public Site ToSite()
|
||||
{
|
||||
return new Site(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/App/Models/Folder.cs
Normal file
29
src/App/Models/Folder.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Bit.App.Models.Data;
|
||||
using Bit.App.Models.Api;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class Folder : Cipher
|
||||
{
|
||||
public Folder()
|
||||
{ }
|
||||
|
||||
public Folder(FolderData data)
|
||||
{
|
||||
Id = data.Id;
|
||||
ServerId = data.ServerId;
|
||||
Name = data.Name != null ? new CipherString(data.Name) : null;
|
||||
}
|
||||
|
||||
public Folder(FolderResponse response)
|
||||
{
|
||||
ServerId = response.Id;
|
||||
Name = response.Name != null ? new CipherString(response.Name) : null;
|
||||
}
|
||||
|
||||
public FolderData ToFolderData()
|
||||
{
|
||||
return new FolderData(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/App/Models/Site.cs
Normal file
47
src/App/Models/Site.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Bit.App.Models.Api;
|
||||
using Bit.App.Models.Data;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class Site : Cipher
|
||||
{
|
||||
public Site()
|
||||
{ }
|
||||
|
||||
public Site(SiteData data)
|
||||
{
|
||||
Id = data.Id;
|
||||
ServerId = data.ServerId;
|
||||
FolderId = data.FolderId;
|
||||
ServerFolderId = data.ServerFolderId;
|
||||
Name = data.Name != null ? new CipherString(data.Name) : null;
|
||||
Uri = data.Uri != null ? new CipherString(data.Uri) : null;
|
||||
Username = data.Username != null ? new CipherString(data.Username) : null;
|
||||
Password = data.Password != null ? new CipherString(data.Password) : null;
|
||||
Notes = data.Notes != null ? new CipherString(data.Notes) : null;
|
||||
}
|
||||
|
||||
public Site(SiteResponse response)
|
||||
{
|
||||
ServerId = response.Id;
|
||||
ServerFolderId = response.FolderId;
|
||||
Name = response.Name != null ? new CipherString(response.Name) : null;
|
||||
Uri = response.Uri != null ? new CipherString(response.Uri) : null;
|
||||
Username = response.Username != null ? new CipherString(response.Username) : null;
|
||||
Password = response.Password != null ? new CipherString(response.Password) : null;
|
||||
Notes = response.Notes != null ? new CipherString(response.Notes) : null;
|
||||
}
|
||||
|
||||
public int? FolderId { get; set; }
|
||||
public string ServerFolderId { get; set; }
|
||||
public CipherString Uri { get; set; }
|
||||
public CipherString Username { get; set; }
|
||||
public CipherString Password { get; set; }
|
||||
public CipherString Notes { get; set; }
|
||||
|
||||
public SiteData ToSiteData()
|
||||
{
|
||||
return new SiteData(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/App/Models/View/VaultView.cs
Normal file
48
src/App/Models/View/VaultView.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Bit.App.Models.View
|
||||
{
|
||||
public class VaultView
|
||||
{
|
||||
public class Site
|
||||
{
|
||||
public Site(Models.Site site)
|
||||
{
|
||||
Id = site.Id;
|
||||
Name = site.Name?.Decrypt();
|
||||
Username = site.Username?.Decrypt();
|
||||
}
|
||||
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Username { get; set; }
|
||||
}
|
||||
|
||||
public class Folder : ObservableCollection<Site>
|
||||
{
|
||||
public Folder(string name) { Name = name; }
|
||||
|
||||
public Folder(IEnumerable<Models.Site> sites)
|
||||
{
|
||||
Name = "(none)";
|
||||
foreach(var site in sites)
|
||||
{
|
||||
Items.Add(new Site(site));
|
||||
}
|
||||
}
|
||||
|
||||
public Folder(Models.Folder folder, IEnumerable<Models.Site> sites)
|
||||
: this(sites)
|
||||
{
|
||||
Id = folder.Id;
|
||||
Name = folder.Name?.Decrypt();
|
||||
}
|
||||
|
||||
public int? Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string FirstLetter { get { return Name.Substring(0, 1); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/App/Pages/LoginPage.cs
Normal file
88
src/App/Pages/LoginPage.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Behaviors;
|
||||
using Bit.App.Models.Api;
|
||||
using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
|
||||
namespace Bit.App.Views
|
||||
{
|
||||
public class LoginPage : ContentPage
|
||||
{
|
||||
public LoginPage()
|
||||
{
|
||||
var cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
var authService = Resolver.Resolve<IAuthService>();
|
||||
|
||||
var emailEntry = new Entry
|
||||
{
|
||||
Keyboard = Keyboard.Email,
|
||||
Placeholder = "Email Address"
|
||||
};
|
||||
|
||||
emailEntry.Behaviors.Add(new RequiredValidationBehavior());
|
||||
emailEntry.Behaviors.Add(new EmailValidationBehavior());
|
||||
|
||||
var masterPasswordEntry = new Entry
|
||||
{
|
||||
IsPassword = true,
|
||||
Placeholder = "Master Password"
|
||||
};
|
||||
|
||||
masterPasswordEntry.Behaviors.Add(new RequiredValidationBehavior());
|
||||
|
||||
var loginButton = new Button
|
||||
{
|
||||
Text = "Log In",
|
||||
Command = new Command(async () =>
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(emailEntry.Text))
|
||||
{
|
||||
await DisplayAlert("An error has occurred", "The Email Address field is required.", "Ok");
|
||||
return;
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(masterPasswordEntry.Text))
|
||||
{
|
||||
await DisplayAlert("An error has occurred", "The Master Password field is required.", "Ok");
|
||||
return;
|
||||
}
|
||||
|
||||
var key = cryptoService.MakeKeyFromPassword(masterPasswordEntry.Text, emailEntry.Text);
|
||||
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = emailEntry.Text,
|
||||
MasterPasswordHash = cryptoService.HashPasswordBase64(key, masterPasswordEntry.Text)
|
||||
};
|
||||
|
||||
var response = await authService.TokenPostAsync(request);
|
||||
if(!response.Succeeded)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
cryptoService.Key = key;
|
||||
authService.Token = response.Result.Token;
|
||||
await Navigation.PushAsync(new VaultListPage());
|
||||
})
|
||||
};
|
||||
|
||||
var stackLayout = new StackLayout
|
||||
{
|
||||
Padding = new Thickness(10, 50, 10, 0)
|
||||
};
|
||||
stackLayout.Children.Add(emailEntry);
|
||||
stackLayout.Children.Add(masterPasswordEntry);
|
||||
stackLayout.Children.Add(loginButton);
|
||||
|
||||
Title = "Log In";
|
||||
Content = stackLayout;
|
||||
NavigationPage.SetHasNavigationBar(this, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/App/Pages/VaultAddFolderPage.cs
Normal file
53
src/App/Pages/VaultAddFolderPage.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
|
||||
namespace Bit.App.Views
|
||||
{
|
||||
public class VaultAddFolderPage : ContentPage
|
||||
{
|
||||
public VaultAddFolderPage()
|
||||
{
|
||||
var cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
var folderService = Resolver.Resolve<IFolderService>();
|
||||
|
||||
var nameEntry = new Entry();
|
||||
|
||||
var stackLayout = new StackLayout();
|
||||
stackLayout.Children.Add(new Label { Text = "Name" });
|
||||
stackLayout.Children.Add(nameEntry);
|
||||
|
||||
var scrollView = new ScrollView
|
||||
{
|
||||
Content = stackLayout,
|
||||
Orientation = ScrollOrientation.Vertical
|
||||
};
|
||||
|
||||
var saveToolBarItem = new ToolbarItem("Save", null, async () =>
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(nameEntry.Text))
|
||||
{
|
||||
await DisplayAlert("An error has occurred", "The Name field is required.", "Ok");
|
||||
return;
|
||||
}
|
||||
|
||||
var folder = new Folder
|
||||
{
|
||||
Name = new CipherString(nameEntry.Text)
|
||||
};
|
||||
await folderService.SaveAsync(folder);
|
||||
await Navigation.PopAsync();
|
||||
}, ToolbarItemOrder.Default, 0);
|
||||
|
||||
Title = "Add Folder";
|
||||
Content = scrollView;
|
||||
ToolbarItems.Add(saveToolBarItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/App/Pages/VaultAddSitePage.cs
Normal file
79
src/App/Pages/VaultAddSitePage.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
|
||||
namespace Bit.App.Views
|
||||
{
|
||||
public class VaultAddSitePage : ContentPage
|
||||
{
|
||||
public VaultAddSitePage()
|
||||
{
|
||||
var cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
var siteService = Resolver.Resolve<ISiteService>();
|
||||
|
||||
var uriEntry = new Entry { Keyboard = Keyboard.Url };
|
||||
var nameEntry = new Entry();
|
||||
var folderEntry = new Entry { };
|
||||
var usernameEntry = new Entry();
|
||||
var passwordEntry = new Entry { IsPassword = true };
|
||||
var notesEditor = new Editor();
|
||||
|
||||
var stackLayout = new StackLayout();
|
||||
stackLayout.Children.Add(new Label { Text = "URI" });
|
||||
stackLayout.Children.Add(uriEntry);
|
||||
stackLayout.Children.Add(new Label { Text = "Name" });
|
||||
stackLayout.Children.Add(nameEntry);
|
||||
stackLayout.Children.Add(new Label { Text = "Folder" });
|
||||
stackLayout.Children.Add(folderEntry);
|
||||
stackLayout.Children.Add(new Label { Text = "Username" });
|
||||
stackLayout.Children.Add(usernameEntry);
|
||||
stackLayout.Children.Add(new Label { Text = "Password" });
|
||||
stackLayout.Children.Add(passwordEntry);
|
||||
stackLayout.Children.Add(new Label { Text = "Notes" });
|
||||
stackLayout.Children.Add(notesEditor);
|
||||
|
||||
var scrollView = new ScrollView
|
||||
{
|
||||
Content = stackLayout,
|
||||
Orientation = ScrollOrientation.Vertical
|
||||
};
|
||||
|
||||
var saveToolBarItem = new ToolbarItem("Save", null, async () =>
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(uriEntry.Text))
|
||||
{
|
||||
await DisplayAlert("An error has occurred", "The Uri field is required.", "Ok");
|
||||
return;
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(nameEntry.Text))
|
||||
{
|
||||
await DisplayAlert("An error has occurred", "The Name field is required.", "Ok");
|
||||
return;
|
||||
}
|
||||
|
||||
var site = new Site
|
||||
{
|
||||
Uri = uriEntry.Text.Encrypt(),
|
||||
Name = nameEntry.Text.Encrypt(),
|
||||
Username = usernameEntry.Text?.Encrypt(),
|
||||
Password = passwordEntry.Text?.Encrypt(),
|
||||
Notes = notesEditor.Text?.Encrypt()
|
||||
};
|
||||
|
||||
await siteService.SaveAsync(site);
|
||||
await Navigation.PopAsync();
|
||||
}, ToolbarItemOrder.Default, 0);
|
||||
|
||||
Title = "Add Site";
|
||||
Content = scrollView;
|
||||
ToolbarItems.Add(saveToolBarItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/App/Pages/VaultEditFolderPage.cs
Normal file
19
src/App/Pages/VaultEditFolderPage.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Views
|
||||
{
|
||||
public class VaultEditFolderPage : ContentPage
|
||||
{
|
||||
public VaultEditFolderPage()
|
||||
{
|
||||
Title = "Edit Folder";
|
||||
Content = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/App/Pages/VaultEditSitePage.cs
Normal file
19
src/App/Pages/VaultEditSitePage.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Views
|
||||
{
|
||||
public class VaultEditSitePage : ContentPage
|
||||
{
|
||||
public VaultEditSitePage()
|
||||
{
|
||||
Title = "Edit Site";
|
||||
Content = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/App/Pages/VaultListPage.cs
Normal file
74
src/App/Pages/VaultListPage.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.View;
|
||||
using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
|
||||
namespace Bit.App.Views
|
||||
{
|
||||
public class VaultListPage : ContentPage
|
||||
{
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ISiteService _siteService;
|
||||
private ListView _listView;
|
||||
|
||||
public VaultListPage()
|
||||
{
|
||||
_folderService = Resolver.Resolve<IFolderService>();
|
||||
_siteService = Resolver.Resolve<ISiteService>();
|
||||
|
||||
var addSiteToolBarItem = new ToolbarItem("+", null, async () =>
|
||||
{
|
||||
var addSitePage = new VaultAddSitePage();
|
||||
await Navigation.PushAsync(addSitePage);
|
||||
}, ToolbarItemOrder.Default, 0);
|
||||
|
||||
ToolbarItems.Add(addSiteToolBarItem);
|
||||
|
||||
_listView = new ListView
|
||||
{
|
||||
IsGroupingEnabled = true,
|
||||
GroupDisplayBinding = new Binding("Name")
|
||||
};
|
||||
|
||||
_listView.ItemSelected += FolderSelected;
|
||||
_listView.ItemTemplate = new DataTemplate(() =>
|
||||
{
|
||||
var cell = new TextCell();
|
||||
cell.SetBinding<VaultView.Site>(TextCell.TextProperty, s => s.Name);
|
||||
cell.SetBinding<VaultView.Site>(TextCell.DetailProperty, s => s.Username);
|
||||
return cell;
|
||||
});
|
||||
|
||||
Title = "My Vault";
|
||||
Content = _listView;
|
||||
NavigationPage.SetHasBackButton(this, false);
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
var folders = _folderService.GetAllAsync().GetAwaiter().GetResult();
|
||||
var sites = _siteService.GetAllAsync().GetAwaiter().GetResult();
|
||||
|
||||
var folderItems = folders.Select(f => new VaultView.Folder(f, sites.Where(s => s.FolderId == f.Id))).ToList();
|
||||
// add the sites with no folder
|
||||
folderItems.Add(new VaultView.Folder(sites.Where(s => !s.FolderId.HasValue)));
|
||||
_listView.ItemsSource = folderItems;
|
||||
}
|
||||
|
||||
void FolderSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void SiteSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/App/Pages/VaultViewSitePage.cs
Normal file
23
src/App/Pages/VaultViewSitePage.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Views
|
||||
{
|
||||
public class VaultViewSitePage : ContentPage
|
||||
{
|
||||
private int _siteId;
|
||||
|
||||
public VaultViewSitePage(int siteId)
|
||||
{
|
||||
_siteId = siteId;
|
||||
|
||||
Title = "View Site";
|
||||
Content = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/App/Properties/AssemblyInfo.cs
Normal file
28
src/App/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Resources;
|
||||
using System.Reflection;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Bit.App")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("bitwarden")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2016")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: NeutralResourcesLanguage("en")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
40
src/App/Services/ApiService.cs
Normal file
40
src/App/Services/ApiService.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
using ModernHttpClient;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class ApiService : IApiService
|
||||
{
|
||||
public ApiService()
|
||||
{
|
||||
Client = new HttpClient(new NativeMessageHandler());
|
||||
Client.BaseAddress = new Uri("https://api.bitwarden.com");
|
||||
}
|
||||
|
||||
public HttpClient Client { get; set; }
|
||||
|
||||
public async Task<ApiResult<T>> HandleErrorAsync<T>(HttpResponseMessage response)
|
||||
{
|
||||
var error = new ApiError
|
||||
{
|
||||
Message = "An unknown error has occured.",
|
||||
StatusCode = response.StatusCode
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
var errorResponseModel = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
|
||||
error.Message = errorResponseModel.Message;
|
||||
}
|
||||
catch(JsonReaderException) { }
|
||||
|
||||
return ApiResult<T>.Failed(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/App/Services/AuthService.cs
Normal file
72
src/App/Services/AuthService.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private const string TokenKey = "token";
|
||||
|
||||
private readonly ISecureStorageService _secureStorage;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IApiService _apiService;
|
||||
|
||||
public AuthService(
|
||||
ISecureStorageService secureStorage,
|
||||
ICryptoService cryptoService,
|
||||
IApiService apiService)
|
||||
{
|
||||
_secureStorage = secureStorage;
|
||||
_cryptoService = cryptoService;
|
||||
_apiService = apiService;
|
||||
}
|
||||
|
||||
public string Token
|
||||
{
|
||||
get
|
||||
{
|
||||
var tokenBytes = _secureStorage.Retrieve(TokenKey);
|
||||
return Encoding.UTF8.GetString(tokenBytes, 0, tokenBytes.Length);
|
||||
}
|
||||
set
|
||||
{
|
||||
if(value != null)
|
||||
{
|
||||
var tokenBytes = Encoding.UTF8.GetBytes(value);
|
||||
_secureStorage.Store(TokenKey, tokenBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
_secureStorage.Delete(TokenKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAuthenticated
|
||||
{
|
||||
get
|
||||
{
|
||||
return _cryptoService.Key != null && Token != null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResult<TokenResponse>> TokenPostAsync(TokenRequest request)
|
||||
{
|
||||
var requestContent = JsonConvert.SerializeObject(request);
|
||||
var response = await _apiService.Client.PostAsync("/auth/token", new StringContent(requestContent, Encoding.UTF8, "application/json"));
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await _apiService.HandleErrorAsync<TokenResponse>(response);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
|
||||
return ApiResult<TokenResponse>.Success(responseObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
166
src/App/Services/CryptoService.cs
Normal file
166
src/App/Services/CryptoService.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Engines;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Modes;
|
||||
using Org.BouncyCastle.Crypto.Paddings;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class CryptoService : ICryptoService
|
||||
{
|
||||
private const string KeyKey = "key";
|
||||
private const int InitializationVectorSize = 16;
|
||||
private const int KeySize = 256;
|
||||
private const int Iterations = 5000;
|
||||
|
||||
private readonly PaddedBufferedBlockCipher _cipher;
|
||||
private readonly ISecureStorageService _secureStorage;
|
||||
private KeyParameter _keyParameter;
|
||||
|
||||
public CryptoService(ISecureStorageService secureStorage)
|
||||
{
|
||||
var engine = new AesEngine();
|
||||
var blockCipher = new CbcBlockCipher(engine);
|
||||
_cipher = new PaddedBufferedBlockCipher(blockCipher);
|
||||
|
||||
_secureStorage = secureStorage;
|
||||
}
|
||||
|
||||
public byte[] Key
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_keyParameter != null)
|
||||
{
|
||||
_keyParameter.GetKey();
|
||||
}
|
||||
|
||||
var storedKey = _secureStorage.Retrieve(KeyKey);
|
||||
if(storedKey == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_keyParameter = new KeyParameter(storedKey);
|
||||
return _keyParameter.GetKey();
|
||||
}
|
||||
set
|
||||
{
|
||||
_secureStorage.Store(KeyKey, value);
|
||||
_keyParameter = new KeyParameter(value);
|
||||
}
|
||||
}
|
||||
public string Base64Key { get { return Convert.ToBase64String(Key); } }
|
||||
|
||||
public CipherString Encrypt(string plaintextValue)
|
||||
{
|
||||
if(Key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(Key));
|
||||
}
|
||||
|
||||
if(plaintextValue == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(plaintextValue));
|
||||
}
|
||||
|
||||
var plaintextBytes = Encoding.UTF8.GetBytes(plaintextValue);
|
||||
|
||||
var iv = GenerateRandomInitializationVector();
|
||||
var keyParamWithIV = new ParametersWithIV(_keyParameter, iv, 0, InitializationVectorSize);
|
||||
|
||||
_cipher.Init(true, keyParamWithIV);
|
||||
var encryptedBytes = new byte[_cipher.GetOutputSize(plaintextBytes.Length)];
|
||||
var length = _cipher.ProcessBytes(plaintextBytes, encryptedBytes, 0);
|
||||
_cipher.DoFinal(encryptedBytes, length);
|
||||
|
||||
return new CipherString(Convert.ToBase64String(iv), Convert.ToBase64String(encryptedBytes));
|
||||
}
|
||||
|
||||
public string Decrypt(CipherString encyptedValue)
|
||||
{
|
||||
if(Key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(Key));
|
||||
}
|
||||
|
||||
if(encyptedValue == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encyptedValue));
|
||||
}
|
||||
|
||||
var keyParamWithIV = new ParametersWithIV(_keyParameter, encyptedValue.InitializationVectorBytes, 0, InitializationVectorSize);
|
||||
|
||||
_cipher.Init(false, keyParamWithIV);
|
||||
byte[] comparisonBytes = new byte[_cipher.GetOutputSize(encyptedValue.CipherTextBytes.Length)];
|
||||
var length = _cipher.ProcessBytes(encyptedValue.CipherTextBytes, comparisonBytes, 0);
|
||||
_cipher.DoFinal(comparisonBytes, length);
|
||||
|
||||
return Encoding.UTF8.GetString(comparisonBytes, 0, comparisonBytes.Length).TrimEnd('\0');
|
||||
}
|
||||
|
||||
public byte[] MakeKeyFromPassword(string password, string salt)
|
||||
{
|
||||
if(password == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
}
|
||||
|
||||
if(salt == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(salt));
|
||||
}
|
||||
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var saltBytes = Encoding.UTF8.GetBytes(salt);
|
||||
|
||||
var generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
|
||||
generator.Init(passwordBytes, saltBytes, Iterations);
|
||||
return ((KeyParameter)generator.GenerateDerivedMacParameters(KeySize)).GetKey();
|
||||
}
|
||||
|
||||
public string MakeKeyFromPasswordBase64(string password, string salt)
|
||||
{
|
||||
var key = MakeKeyFromPassword(password, salt);
|
||||
return Convert.ToBase64String(key);
|
||||
}
|
||||
|
||||
public byte[] HashPassword(byte[] key, string password)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(Key));
|
||||
}
|
||||
|
||||
if(password == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
}
|
||||
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
|
||||
var generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
|
||||
generator.Init(key, passwordBytes, 1);
|
||||
return ((KeyParameter)generator.GenerateDerivedMacParameters(KeySize)).GetKey();
|
||||
}
|
||||
|
||||
public string HashPasswordBase64(byte[] key, string password)
|
||||
{
|
||||
var hash = HashPassword(key, password);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
private byte[] GenerateRandomInitializationVector()
|
||||
{
|
||||
var rand = new Random();
|
||||
var iv = new byte[InitializationVectorSize];
|
||||
rand.NextBytes(iv);
|
||||
return iv;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/App/Services/DatabaseService.cs
Normal file
23
src/App/Services/DatabaseService.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Data;
|
||||
using SQLite;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class DatabaseService : IDatabaseService
|
||||
{
|
||||
protected readonly SQLiteConnection _connection;
|
||||
|
||||
public DatabaseService(ISqlService sqlService)
|
||||
{
|
||||
_connection = sqlService.GetConnection();
|
||||
}
|
||||
|
||||
public void CreateTables()
|
||||
{
|
||||
_connection.CreateTable<FolderData>();
|
||||
_connection.CreateTable<SiteData>();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/App/Services/FolderService.cs
Normal file
39
src/App/Services/FolderService.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Models.Data;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class FolderService : Repository<FolderData, int>, IFolderService
|
||||
{
|
||||
public FolderService(ISqlService sqlite)
|
||||
: base(sqlite) { }
|
||||
|
||||
public new async Task<IEnumerable<Folder>> GetAllAsync()
|
||||
{
|
||||
var data = await base.GetAllAsync();
|
||||
return data.Select(f => new Folder(f));
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Folder folder)
|
||||
{
|
||||
var data = new FolderData(folder);
|
||||
data.RevisionDateTime = DateTime.UtcNow;
|
||||
|
||||
if(folder.Id == 0)
|
||||
{
|
||||
await CreateAsync(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplaceAsync(data);
|
||||
}
|
||||
|
||||
folder.Id = data.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/App/Services/Repository.cs
Normal file
49
src/App/Services/Repository.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using SQLite;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public abstract class Repository<T, TId>
|
||||
where TId : IEquatable<TId>
|
||||
where T : class, IDataObject<TId>, new()
|
||||
{
|
||||
protected readonly SQLiteConnection _connection;
|
||||
|
||||
public Repository(ISqlService sqlite)
|
||||
{
|
||||
_connection = sqlite.GetConnection();
|
||||
}
|
||||
|
||||
protected virtual Task<T> GetByIdAsync(TId id)
|
||||
{
|
||||
return Task.FromResult(_connection.Get<T>(id));
|
||||
}
|
||||
|
||||
protected virtual Task<IEnumerable<T>> GetAllAsync()
|
||||
{
|
||||
return Task.FromResult(_connection.Table<T>().Cast<T>());
|
||||
}
|
||||
|
||||
protected virtual Task CreateAsync(T obj)
|
||||
{
|
||||
_connection.Insert(obj);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
protected virtual Task ReplaceAsync(T obj)
|
||||
{
|
||||
_connection.Update(obj);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
protected virtual Task DeleteAsync(T obj)
|
||||
{
|
||||
_connection.Delete<T>(obj.Id);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/App/Services/SiteService.cs
Normal file
39
src/App/Services/SiteService.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Models.Data;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class SiteService : Repository<SiteData, int>, ISiteService
|
||||
{
|
||||
public SiteService(ISqlService sqlite)
|
||||
: base(sqlite) { }
|
||||
|
||||
public new async Task<IEnumerable<Site>> GetAllAsync()
|
||||
{
|
||||
var data = await base.GetAllAsync();
|
||||
return data.Select(s => new Site(s));
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Site site)
|
||||
{
|
||||
var data = new SiteData(site);
|
||||
data.RevisionDateTime = DateTime.UtcNow;
|
||||
|
||||
if(site.Id == 0)
|
||||
{
|
||||
await CreateAsync(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplaceAsync(data);
|
||||
}
|
||||
|
||||
site.Id = data.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/App/Utilities/Extentions.cs
Normal file
21
src/App/Utilities/Extentions.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using XLabs.Ioc;
|
||||
|
||||
namespace Bit.App
|
||||
{
|
||||
public static class Extentions
|
||||
{
|
||||
public static CipherString Encrypt(this string s)
|
||||
{
|
||||
if(s == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(s));
|
||||
}
|
||||
|
||||
var cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
return cryptoService.Encrypt(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/App/packages.config
Normal file
12
src/App/packages.config
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="CommonServiceLocator" version="1.3" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="modernhttpclient" version="2.4.2" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Newtonsoft.Json" version="8.0.3" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Portable.BouncyCastle" version="1.8.1" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="sqlite-net-pcl" version="1.1.1" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="SQLitePCL.raw" version="0.8.6" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Unity" version="3.5.1405-prerelease" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="Xamarin.Forms" version="2.2.0.31" targetFramework="portable45-net45+win8+wpa81" />
|
||||
<package id="XLabs.IoC" version="2.0.5782" targetFramework="portable45-net45+win8+wpa81" />
|
||||
</packages>
|
||||
Reference in New Issue
Block a user