diff --git a/src/App/App.csproj b/src/App/App.csproj index 131c2d56f..c90ae7fee 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -87,6 +87,7 @@ + @@ -98,7 +99,8 @@ - + + @@ -123,6 +125,7 @@ + diff --git a/src/App/Enums/CipherType.cs b/src/App/Enums/CipherType.cs index 4cef54436..41950bc79 100644 --- a/src/App/Enums/CipherType.cs +++ b/src/App/Enums/CipherType.cs @@ -4,6 +4,8 @@ { // Folder deprecated //Folder = 0, - Login = 1 + Login = 1, + SecureNote = 2, + Card = 3 } } diff --git a/src/App/Enums/FieldType.cs b/src/App/Enums/FieldType.cs new file mode 100644 index 000000000..50d9251ee --- /dev/null +++ b/src/App/Enums/FieldType.cs @@ -0,0 +1,9 @@ +namespace Bit.App.Enums +{ + public enum FieldType : byte + { + Text = 0, + Hidden = 1, + Boolean = 2 + } +} diff --git a/src/App/Models/Api/CipherDataModel.cs b/src/App/Models/Api/CipherDataModel.cs new file mode 100644 index 000000000..81dbf8d6c --- /dev/null +++ b/src/App/Models/Api/CipherDataModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Bit.App.Models.Api +{ + public abstract class CipherDataModel + { + public string Name { get; set; } + public string Notes { get; set; } + public IEnumerable Fields { get; set; } + } +} diff --git a/src/App/Models/Api/FieldDataModel.cs b/src/App/Models/Api/FieldDataModel.cs new file mode 100644 index 000000000..1ee190a6c --- /dev/null +++ b/src/App/Models/Api/FieldDataModel.cs @@ -0,0 +1,11 @@ +using Bit.App.Enums; + +namespace Bit.App.Models.Api +{ + public class FieldDataModel + { + public FieldType Type { get; set; } + public string Name { get; set; } + public string Value { get; set; } + } +} diff --git a/src/App/Models/Api/FolderDataModel.cs b/src/App/Models/Api/FolderDataModel.cs deleted file mode 100644 index a5d7d7b6e..000000000 --- a/src/App/Models/Api/FolderDataModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bit.App.Models.Api -{ - public class FolderDataModel - { - public string Name { get; set; } - } -} diff --git a/src/App/Models/Api/LoginDataModel.cs b/src/App/Models/Api/LoginDataModel.cs index 2791257b1..637822256 100644 --- a/src/App/Models/Api/LoginDataModel.cs +++ b/src/App/Models/Api/LoginDataModel.cs @@ -1,12 +1,10 @@ namespace Bit.App.Models.Api { - public class LoginDataModel + public class LoginDataModel : CipherDataModel { - 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 string Totp { get; set; } } } diff --git a/src/App/Models/Data/LoginData.cs b/src/App/Models/Data/LoginData.cs index 0896e588a..9bd763000 100644 --- a/src/App/Models/Data/LoginData.cs +++ b/src/App/Models/Data/LoginData.cs @@ -2,6 +2,8 @@ using SQLite; using Bit.App.Abstractions; using Bit.App.Models.Api; +using Newtonsoft.Json; +using System.Linq; namespace Bit.App.Models.Data { @@ -11,23 +13,6 @@ namespace Bit.App.Models.Data public LoginData() { } - public LoginData(Login login, string userId) - { - Id = login.Id; - FolderId = login.FolderId; - UserId = userId; - OrganizationId = login.OrganizationId; - Name = login.Name?.EncryptedString; - Uri = login.Uri?.EncryptedString; - Username = login.Username?.EncryptedString; - Password = login.Password?.EncryptedString; - Notes = login.Notes?.EncryptedString; - Totp = login?.Notes?.EncryptedString; - Favorite = login.Favorite; - Edit = login.Edit; - OrganizationUseTotp = login.OrganizationUseTotp; - } - public LoginData(CipherResponse cipher, string userId) { if(cipher.Type != Enums.CipherType.Login) @@ -51,6 +36,15 @@ namespace Bit.App.Models.Data Edit = cipher.Edit; OrganizationUseTotp = cipher.OrganizationUseTotp; RevisionDateTime = cipher.RevisionDate; + + if(data.Fields != null && data.Fields.Any()) + { + try + { + Fields = JsonConvert.SerializeObject(data.Fields); + } + catch(JsonSerializationException) { } + } } [PrimaryKey] @@ -65,6 +59,7 @@ namespace Bit.App.Models.Data public string Password { get; set; } public string Notes { get; set; } public string Totp { get; set; } + public string Fields { get; set; } public bool Favorite { get; set; } public bool Edit { get; set; } public bool OrganizationUseTotp { get; set; } diff --git a/src/App/Models/Field.cs b/src/App/Models/Field.cs new file mode 100644 index 000000000..c0df3167e --- /dev/null +++ b/src/App/Models/Field.cs @@ -0,0 +1,19 @@ +using Bit.App.Enums; +using Bit.App.Models.Api; + +namespace Bit.App.Models +{ + public class Field + { + public Field(FieldDataModel model) + { + Type = model.Type; + Name = new CipherString(model.Name); + Value = new CipherString(model.Value); + } + + public FieldType Type { get; set; } + public CipherString Name { get; set; } + public CipherString Value { get; set; } + } +} diff --git a/src/App/Models/Login.cs b/src/App/Models/Login.cs index de624a105..690e17616 100644 --- a/src/App/Models/Login.cs +++ b/src/App/Models/Login.cs @@ -1,5 +1,6 @@ using Bit.App.Models.Api; using Bit.App.Models.Data; +using Newtonsoft.Json; using System.Collections.Generic; using System.Linq; @@ -26,6 +27,16 @@ namespace Bit.App.Models Edit = data.Edit; OrganizationUseTotp = data.OrganizationUseTotp; Attachments = attachments?.Select(a => new Attachment(a)); + + if(!string.IsNullOrWhiteSpace(data.Fields)) + { + try + { + var fieldModels = JsonConvert.DeserializeObject>(data.Fields); + Fields = fieldModels?.Select(f => new Field(f)); + } + catch(JsonSerializationException) { } + } } public string Id { get; set; } @@ -38,14 +49,10 @@ namespace Bit.App.Models public CipherString Password { get; set; } public CipherString Notes { get; set; } public CipherString Totp { get; set; } + public IEnumerable Fields { get; set; } public bool Favorite { get; set; } public bool Edit { get; set; } public bool OrganizationUseTotp { get; set; } public IEnumerable Attachments { get; set; } - - public LoginData ToLoginData(string userId) - { - return new LoginData(this, userId); - } } } diff --git a/src/App/Models/Page/VaultViewLoginPageModel.cs b/src/App/Models/Page/VaultViewLoginPageModel.cs index 848bde56f..bc3261568 100644 --- a/src/App/Models/Page/VaultViewLoginPageModel.cs +++ b/src/App/Models/Page/VaultViewLoginPageModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using Bit.App.Resources; using Xamarin.Forms; using System.Collections.Generic; +using Bit.App.Enums; namespace Bit.App.Models.Page { @@ -17,6 +18,7 @@ namespace Bit.App.Models.Page private int _totpSec = 30; private bool _revealPassword; private List _attachments; + private List _fields; public VaultViewLoginPageModel() { } @@ -144,12 +146,10 @@ namespace Bit.App.Models.Page _revealPassword = value; PropertyChanged(this, new PropertyChangedEventArgs(nameof(RevealPassword))); PropertyChanged(this, new PropertyChangedEventArgs(nameof(MaskedPassword))); - PropertyChanged(this, new PropertyChangedEventArgs(nameof(ShowHideText))); PropertyChanged(this, new PropertyChangedEventArgs(nameof(ShowHideImage))); } } public string MaskedPassword => RevealPassword ? Password : Password == null ? null : new string('●', Password.Length); - public string ShowHideText => RevealPassword ? AppResources.Hide : AppResources.Show; public ImageSource ShowHideImage => RevealPassword ? ImageSource.FromFile("eye_slash") : ImageSource.FromFile("eye"); public string TotpCode @@ -189,6 +189,18 @@ namespace Bit.App.Models.Page } public bool ShowAttachments => (Attachments?.Count ?? 0) > 0; + public List Fields + { + get { return _fields; } + set + { + _fields = value; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Fields))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(ShowFields))); + } + } + public bool ShowFields => (Fields?.Count ?? 0) > 0; + public void Update(Login login) { Name = login.Name?.Decrypt(login.OrganizationId); @@ -217,6 +229,25 @@ namespace Bit.App.Models.Page { login.Attachments = null; } + + if(login.Fields != null) + { + var fields = new List(); + foreach(var field in login.Fields) + { + fields.Add(new Field + { + Name = field.Name?.Decrypt(login.OrganizationId), + Value = field.Value?.Decrypt(login.OrganizationId), + Type = field.Type + }); + } + Fields = fields; + } + else + { + login.Fields = null; + } } public class Attachment @@ -227,5 +258,26 @@ namespace Bit.App.Models.Page public long Size { get; set; } public string Url { get; set; } } + + public class Field + { + private string _maskedValue; + + public string Name { get; set; } + public string Value { get; set; } + public string MaskedValue + { + get + { + if(_maskedValue == null && Value != null) + { + _maskedValue = new string('●', Value.Length); + } + + return _maskedValue; + } + } + public FieldType Type { get; set; } + } } } diff --git a/src/App/Pages/Vault/VaultViewLoginPage.cs b/src/App/Pages/Vault/VaultViewLoginPage.cs index 263c68f6e..8384c70d1 100644 --- a/src/App/Pages/Vault/VaultViewLoginPage.cs +++ b/src/App/Pages/Vault/VaultViewLoginPage.cs @@ -11,6 +11,7 @@ using Bit.App.Utilities; using System.Collections.Generic; using Bit.App.Models; using System.Linq; +using Bit.App.Enums; namespace Bit.App.Pages { @@ -39,12 +40,14 @@ namespace Bit.App.Pages private TableSection LoginInformationSection { get; set; } private TableSection NotesSection { get; set; } private TableSection AttachmentsSection { get; set; } + private TableSection FieldsSection { get; set; } public LabeledValueCell UsernameCell { get; set; } public LabeledValueCell PasswordCell { get; set; } public LabeledValueCell UriCell { get; set; } public LabeledValueCell NotesCell { get; set; } public LabeledValueCell TotpCodeCell { get; set; } private EditLoginToolBarItem EditItem { get; set; } + public List FieldsCells { get; set; } public List AttachmentCells { get; set; } private void Init() @@ -255,6 +258,53 @@ namespace Bit.App.Pages Table.Root.Add(AttachmentsSection); } + if(Table.Root.Contains(FieldsSection)) + { + Table.Root.Remove(FieldsSection); + } + if(Model.ShowFields) + { + FieldsSection = new TableSection(AppResources.CustomFields); + foreach(var field in Model.Fields) + { + LabeledValueCell fieldCell; + ExtendedButton copyButton = null; + switch(field.Type) + { + case FieldType.Text: + fieldCell = new LabeledValueCell(field.Name, field.Value, AppResources.Copy); + copyButton = fieldCell.Button1; + break; + case FieldType.Hidden: + fieldCell = new LabeledValueCell(field.Name, field.MaskedValue, string.Empty, AppResources.Copy); + copyButton = fieldCell.Button2; + fieldCell.Value.FontFamily = + Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", WinPhone: "Courier"); + if(Device.RuntimePlatform == Device.iOS) + { + fieldCell.Button1.Margin = new Thickness(10, 0); + } + // TODO: show/hide image toggle + break; + case FieldType.Boolean: + fieldCell = new LabeledValueCell(field.Name, field.Value == "true" ? "✓" : "-"); + break; + default: + continue; + } + + fieldCell.Value.LineBreakMode = LineBreakMode.WordWrap; + if(copyButton != null) + { + copyButton.Command = new Command(() => Copy(field.Value, field.Name)); + copyButton.WidthRequest = 59; + } + + FieldsSection.Add(fieldCell); + } + Table.Root.Add(FieldsSection); + } + base.OnAppearing(); } @@ -322,8 +372,8 @@ namespace Bit.App.Pages private void Copy(string copyText, string alertLabel) { - _deviceActionService.CopyToClipboard(copyText); - _userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel)); + _deviceActionService.CopyToClipboard(copyText); + _userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel)); } private void TotpTick(string totpKey) diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 69b152f38..8bec4193f 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -718,6 +718,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Custom Fields. + /// + public static string CustomFields { + get { + return ResourceManager.GetString("CustomFields", resourceCulture); + } + } + /// /// Looks up a localized string similar to Delete. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index bbd62447d..2eb540bae 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1030,4 +1030,7 @@ Tap this notification to view logins from your vault. + + Custom Fields + \ No newline at end of file