private static APIResultSync GetCursorResult(ISimpleJsonRest web, StandardNoteData dat, APIBodySync d) { var masterResult = new APIResultSync { retrieved_items = new List <APIResultItem>(), unsaved = new List <APIResultErrorItem>(), saved_items = new List <APIResultItem>() }; for (;;) { var result = web.PostTwoWay <APIResultSync>(d, "items/sync"); dat.SyncToken = result.sync_token.Trim(); masterResult.sync_token = result.sync_token; masterResult.unsaved.AddRange(result.unsaved); masterResult.cursor_token = result.cursor_token; masterResult.retrieved_items.AddRange(result.retrieved_items); masterResult.saved_items.AddRange(result.saved_items); if (result.cursor_token == null) { return(masterResult); } d.cursor_token = result.cursor_token; d.items.Clear(); } }
private static StandardFileItemsKey GetDefaultItemsKey(StandardNoteData dat, string version) { if (dat.ItemsKeys.Where(p => p.Version == version).Count() == 0) { throw new StandardNoteAPIException("Could not encrypt item, no items_key in repository"); } if (dat.ItemsKeys.Where(p => p.Version == version).Count() == 1) { return(dat.ItemsKeys.Single(p => p.Version == version)); } var def = dat.ItemsKeys.FirstOrDefault(p => p.Version == version && p.IsDefault); if (def != null) { return(def); } StandardNoteAPI.Logger.Warn(StandardNotePlugin.Name, "No default key for encryption specified (using latest)", $"Keys in storage: {dat.ItemsKeys.Count}"); var latest = dat.ItemsKeys.Where(p => p.Version == version).OrderBy(p => p.CreationDate).Last(); return(latest); }
private ISimpleJsonRest CreateAuthenticatedClient(StandardNoteData dat) { RefreshToken(dat); var client = CreateJsonRestClient(_proxy, _config.Server); client.AddHeader("Authorization", "Bearer " + dat.SessionData.Token); client.AddDTOConverter(ConvertToDTO, ConvertFromDTO); return(client); }
private void RefreshToken(StandardNoteData dat) { try { if (dat.SessionData == null || dat.SessionData.AccessExpiration <= DateTime.Now) { dat.SessionData = null; } if (dat.SessionData != null) { if (dat.SessionData.AccessExpiration != null) { _logger.Debug(StandardNotePlugin.Name, $"Reusing existing token (until {dat.SessionData.AccessExpiration:yyyy-MM-dd HH:mm})", dat.SessionData.Token); } else { _logger.Debug(StandardNotePlugin.Name, $"Reusing existing token (no expiration)", dat.SessionData.Token); } return; } using (var web = CreateJsonRestClient(_proxy, _config.Server)) { _logger.Debug(StandardNotePlugin.Name, "Requesting token from StandardNoteServer"); dat.SessionData = StandardNoteAPI.Authenticate(web, _config.Email, _config.Password, _logger); _logger.Debug(StandardNotePlugin.Name, $"StandardNoteServer returned token \"{dat.SessionData.Token}\" (until {dat.SessionData.AccessExpiration:yyyy-MM-dd HH:mm:ss})"); } } catch (StandardNoteAPIException) { throw; } catch (RestException) { throw; } catch (Exception e) { throw new StandardNoteAPIException("Could not authenticate with StandardNoteServer : " + e.Message, e); } }
public static EncryptResult EncryptContent(string content, Guid uuid, StandardNoteData dat) { if (dat.SessionData.Version == "001") { return(EncryptContent001(content, dat.SessionData.RootKey_MasterKey)); } if (dat.SessionData.Version == "002") { return(EncryptContent002(content, uuid, dat.SessionData.RootKey_MasterKey, dat.SessionData.RootKey_MasterAuthKey)); } if (dat.SessionData.Version == "003") { return(EncryptContent003(content, uuid, dat.SessionData.RootKey_MasterKey, dat.SessionData.RootKey_MasterAuthKey)); } if (dat.SessionData.Version == "004") { return(EncryptContent004(content, uuid, dat)); } if (dat.SessionData.Version == "005") { throw new StandardNoteAPIException("Unsupported encryption scheme 005 in note content"); } if (dat.SessionData.Version == "006") { throw new StandardNoteAPIException("Unsupported encryption scheme 006 in note content"); } if (dat.SessionData.Version == "007") { throw new StandardNoteAPIException("Unsupported encryption scheme 007 in note content"); } if (dat.SessionData.Version == "008") { throw new StandardNoteAPIException("Unsupported encryption scheme 008 in note content"); } if (dat.SessionData.Version == "009") { throw new StandardNoteAPIException("Unsupported encryption scheme 009 in note content"); } if (dat.SessionData.Version == "010") { throw new StandardNoteAPIException("Unsupported encryption scheme 010 in note content"); } throw new Exception("Unsupported encryption scheme: " + dat.SessionData.Version); }
private static EncryptResult EncryptContent004(string rawContent, Guid uuid, StandardNoteData dat) { var item_key = RandomSeed(32); var authenticated_data = $"{{\"u\":\"{uuid:D}\",\"v\":\"004\"}}"; var encrypted_content = Encrypt004(rawContent, EncodingConverter.StringToByteArrayCaseInsensitive(item_key), authenticated_data); var default_items_key = GetDefaultItemsKey(dat, "004"); var enc_item_key = Encrypt004(item_key, default_items_key.Key, authenticated_data); return(new EncryptResult { enc_item_key = enc_item_key, enc_content = encrypted_content, auth_hash = null, items_key_id = default_items_key.UUID, }); }
private static string DecryptContent001(string encContent, string encItemKey, string authHash, StandardNoteData dat) { StandardNoteAPI.Logger.TraceExt(StandardNotePlugin.Name, "Decrypt content with schema [001]", ("encContent", encContent), ("encItemKey", encItemKey), ("authHash", authHash)); byte[] masterkey; if (dat.SessionData.Version == "001" || dat.SessionData.Version == "002" || dat.SessionData.Version == "003") { masterkey = dat.SessionData.RootKey_MasterKey; StandardNoteAPI.Logger.Trace(StandardNotePlugin.Name, "Use masterkey from session"); } else { var itemskey = dat.ItemsKeys.FirstOrDefault(p => p.Version == "001"); if (itemskey == null) { throw new StandardNoteAPIException($"Could not decrypt item (Key for 002 not found)"); } StandardNoteAPI.Logger.TraceExt(StandardNotePlugin.Name, $"Found itemskey: {itemskey.UUID}", ("itemskey.IsDefault", itemskey.IsDefault.ToString()), ("itemskey.Version", itemskey.Version), ("itemskey.Key", EncodingConverter.ByteToHexBitFiddleLowercase(itemskey.Key)), ("itemskey.AuthKey", EncodingConverter.ByteToHexBitFiddleLowercase(itemskey.AuthKey))); masterkey = itemskey.Key; } var itemKey = EncodingConverter.StringToByteArrayCaseInsensitive(Encoding.ASCII.GetString(AESEncryption.DecryptCBC256(Convert.FromBase64String(encItemKey), masterkey, new byte[16]))); var ek = itemKey.Take(itemKey.Length / 2).ToArray(); var ak = itemKey.Skip(itemKey.Length / 2).ToArray(); var realHash = EncodingConverter.ByteToHexBitFiddleLowercase(AuthSHA256(Encoding.UTF8.GetBytes(encContent), ak)); if (authHash == null) { throw new ArgumentNullException(nameof(authHash)); } if (realHash.ToLower() != authHash.ToLower()) { throw new StandardNoteAPIException("Decrypting content failed - hash mismatch"); } var c = AESEncryption.DecryptCBC256(Convert.FromBase64String(encContent.Substring(3)), ek, null); return(Encoding.UTF8.GetString(c)); }
private static string DecryptContent004(string encContent, string encItemKey, Guid?itemsKeyID, StandardNoteData dat) { StandardNoteAPI.Logger.TraceExt(StandardNotePlugin.Name, "Decrypt content with schema [002]", ("encContent", encContent), ("encItemKey", encItemKey), ("itemsKeyID", itemsKeyID?.ToString() ?? "NULL")); var keyOuter = dat.SessionData.RootKey_MasterKey; if (itemsKeyID != null) { var itemskey = dat.ItemsKeys.FirstOrDefault(p => p.UUID == itemsKeyID); if (itemskey == null) { throw new StandardNoteAPIException($"Could not decrypt item (Key {itemsKeyID} not found)"); } StandardNoteAPI.Logger.TraceExt(StandardNotePlugin.Name, $"Found itemskey: {itemskey.UUID}", ("itemskey.IsDefault", itemskey.IsDefault.ToString()), ("itemskey.Version", itemskey.Version), ("itemskey.Key", EncodingConverter.ByteToHexBitFiddleLowercase(itemskey.Key))); keyOuter = itemskey.Key; } var keyInner = Decrypt004(encItemKey, keyOuter); return(Decrypt004(encContent, EncodingConverter.StringToByteArrayCaseInsensitive(keyInner))); }
private static string DecryptContent003(string encContent, string encItemKey, StandardNoteData dat) { StandardNoteAPI.Logger.TraceExt(StandardNotePlugin.Name, "Decrypt content with schema [002]", ("encContent", encContent), ("encItemKey", encItemKey)); byte[] masterMK; byte[] masterAK; if (dat.SessionData.Version == "001" || dat.SessionData.Version == "002" || dat.SessionData.Version == "003") { masterMK = dat.SessionData.RootKey_MasterKey; masterAK = dat.SessionData.RootKey_MasterAuthKey; StandardNoteAPI.Logger.Trace(StandardNotePlugin.Name, "Use key/authkey from session"); } else { var itemskey = dat.ItemsKeys.FirstOrDefault(p => p.Version == "003"); if (itemskey == null) { throw new StandardNoteAPIException($"Could not decrypt item (Key for 002 not found)"); } StandardNoteAPI.Logger.TraceExt(StandardNotePlugin.Name, $"Found itemskey: {itemskey.UUID}", ("itemskey.IsDefault", itemskey.IsDefault.ToString()), ("itemskey.Version", itemskey.Version), ("itemskey.Key", EncodingConverter.ByteToHexBitFiddleLowercase(itemskey.Key)), ("itemskey.AuthKey", EncodingConverter.ByteToHexBitFiddleLowercase(itemskey.AuthKey))); masterMK = itemskey.Key; masterAK = itemskey.AuthKey; } var item_key = Decrypt003(encItemKey, masterMK, masterAK); StandardNoteAPI.Logger.Trace(StandardNotePlugin.Name, "item_key decrypted", $"item_key := '{item_key}'"); var item_ek = item_key.Substring(0, item_key.Length / 2); var item_ak = item_key.Substring(item_key.Length / 2, item_key.Length / 2); return(Decrypt003(encContent, EncodingConverter.StringToByteArrayCaseInsensitive(item_ek), EncodingConverter.StringToByteArrayCaseInsensitive(item_ak))); }
public static string DecryptContent(string encContent, string encItemKey, Guid?itemsKeyID, string authHash, StandardNoteData dat) { if (encContent.StartsWith("000")) { return(DecryptContent000(encContent)); } if (encContent.StartsWith("001")) { return(DecryptContent001(encContent, encItemKey, authHash, dat)); } if (encContent.StartsWith("002")) { return(DecryptContent002(encContent, encItemKey, dat)); } if (encContent.StartsWith("003")) { return(DecryptContent003(encContent, encItemKey, dat)); } if (encContent.StartsWith("004")) { return(DecryptContent004(encContent, encItemKey, itemsKeyID, dat)); } if (encContent.StartsWith("005")) { throw new StandardNoteAPIException("Unsupported encryption scheme 005 in note content"); } if (encContent.StartsWith("006")) { throw new StandardNoteAPIException("Unsupported encryption scheme 006 in note content"); } if (encContent.StartsWith("007")) { throw new StandardNoteAPIException("Unsupported encryption scheme 007 in note content"); } if (encContent.StartsWith("008")) { throw new StandardNoteAPIException("Unsupported encryption scheme 008 in note content"); } if (encContent.StartsWith("009")) { throw new StandardNoteAPIException("Unsupported encryption scheme 009 in note content"); } if (encContent.StartsWith("010")) { throw new StandardNoteAPIException("Unsupported encryption scheme 010 in note content"); } throw new StandardNoteAPIException("Unsupported encryption scheme ? in note content"); }
private static StandardFileNote CreateNote(ISimpleJsonRest web, StandardNoteConnection conn, APIResultItem encNote, APIResultAuthorize authToken, StandardNoteConfig cfg, StandardNoteData dat) { if (encNote.deleted) { var nd = new StandardFileNote(encNote.uuid, cfg, conn.HConfig) { CreationDate = encNote.created_at, Text = "", InternalTitle = "", AuthHash = encNote.auth_hash, ContentVersion = StandardNoteCrypt.GetSchemaVersion(encNote.content), }; nd.ModificationDate = encNote.updated_at; return(nd); } ContentNote content; try { var contentJson = StandardNoteCrypt.DecryptContent(encNote.content, encNote.enc_item_key, encNote.auth_hash, authToken.masterkey, authToken.masterauthkey); Logger.Debug( StandardNotePlugin.Name, $"DecryptContent of note {encNote.uuid:B}", $"[content]:\r\n{encNote.content}\r\n" + $"[enc_item_key]:\r\n{encNote.enc_item_key}\r\n" + $"[auth_hash]:\r\n{encNote.auth_hash}\r\n" + $"\r\n\r\n" + $"[contentJson]:\r\n{contentJson}\r\n"); content = web.ParseJsonWithoutConverter <ContentNote>(contentJson); } catch (RestException) { throw; } catch (Exception e) { throw new StandardNoteAPIException("Cannot decrypt note with local masterkey", e); } var n = new StandardFileNote(encNote.uuid, cfg, conn.HConfig) { Text = StandardNoteConfig.REX_LINEBREAK.Replace(content.text, Environment.NewLine), InternalTitle = content.title, AuthHash = encNote.auth_hash, ContentVersion = StandardNoteCrypt.GetSchemaVersion(encNote.content), IsPinned = GetAppDataBool(content.appData, APPDATA_PINNED, false), IsLocked = GetAppDataBool(content.appData, APPDATA_LOCKED, false), }; var refTags = new List <StandardFileTagRef>(); foreach (var cref in content.references) { if (cref.content_type == "Note") { // ignore } else if (dat.Tags.Any(t => t.UUID == cref.uuid)) { refTags.Add(new StandardFileTagRef(cref.uuid, dat.Tags.First(t => t.UUID == cref.uuid).Title)); } else if (cref.content_type == "Tag") { Logger.Warn(StandardNotePlugin.Name, $"Reference to missing tag {cref.uuid} in note {encNote.uuid}"); } else { Logger.Error(StandardNotePlugin.Name, $"Downloaded note contains an unknown reference :{cref.uuid} ({cref.content_type}) in note {encNote.uuid}"); } } foreach (var tref in dat.Tags.Where(tag => tag.References.Any(tref => tref == encNote.uuid))) { refTags.Add(new StandardFileTagRef(tref.UUID, tref.Title)); } refTags = refTags.DistinctBy(t => t.UUID).ToList(); n.SetTags(refTags); n.SetReferences(content.references); n.CreationDate = encNote.created_at; n.ModificationDate = encNote.updated_at; return(n); }
public static SyncResult Sync(ISimpleJsonRest web, StandardNoteConnection conn, APIResultAuthorize authToken, StandardNoteConfig cfg, StandardNoteData dat, List <StandardFileNote> allNotes, List <StandardFileNote> notesUpload, List <StandardFileNote> notesDelete, List <StandardFileTag> tagsDelete) { APIBodySync d = new APIBodySync(); d.cursor_token = null; d.sync_token = string.IsNullOrWhiteSpace(dat.SyncToken) ? null : dat.SyncToken; d.limit = 150; d.items = new List <APIBodyItem>(); var items_raw = new List <APIRawBodyItem>(); var allTags = dat.Tags.ToList(); // Upload new notes foreach (var mvNote in notesUpload) { PrepareNoteForUpload(web, d, ref items_raw, mvNote, allTags, authToken, cfg, false); } // Delete deleted notes foreach (var rmNote in notesDelete) { PrepareNoteForUpload(web, d, ref items_raw, rmNote, allTags, authToken, cfg, true); } // Update references on tags (from changed notes) foreach (var upTag in GetTagsInNeedOfUpdate(dat.Tags, notesUpload, notesDelete)) { PrepareTagForUpload(web, d, ref items_raw, upTag, authToken, false); } // Remove unused tags if (cfg.RemEmptyTags) { foreach (var rmTag in tagsDelete) { PrepareTagForUpload(web, d, ref items_raw, rmTag, authToken, true); } } Logger.Debug( StandardNotePlugin.Name, $"Perform sync request ({items_raw.Count} items send)", "Sent Items (unencrypted):\n\n" + string.Join("\n", items_raw.Select(i => $"{{\n content_type = {i.content_type}\n uuid = {i.uuid}\n created_at = {i.created_at}\n deleted = {i.deleted}\n content =\n{CompactJsonFormatter.FormatJSON(i.content, 2, 1)}\n}}"))); var result = GetCursorResult(web, dat, d); var syncresult = new SyncResult(); syncresult.retrieved_tags = result .retrieved_items .Where(p => p.content_type.ToLower() == "tag") .Where(p => !p.deleted) .Select(n => CreateTag(web, n, authToken)) .ToList(); syncresult.deleted_tags = result .retrieved_items .Where(p => p.content_type.ToLower() == "tag") .Where(p => p.deleted) .Select(n => CreateTag(web, n, authToken)) .ToList(); syncresult.saved_tags = result .saved_items .Where(p => p.content_type.ToLower() == "tag") .Select(n => CreateTag(web, n, authToken)) .ToList(); syncresult.unsaved_tags = result .unsaved .Where(p => p.item.content_type.ToLower() == "tag") .Select(n => CreateTag(web, n.item, authToken)) .ToList(); dat.UpdateTags(syncresult.retrieved_tags, syncresult.saved_tags, syncresult.unsaved_tags, syncresult.deleted_tags); syncresult.retrieved_notes = result .retrieved_items .Where(p => p.content_type.ToLower() == "note") .Where(p => !p.deleted) .Select(n => CreateNote(web, conn, n, authToken, cfg, dat)) .ToList(); syncresult.deleted_notes = result .retrieved_items .Where(p => p.content_type.ToLower() == "note") .Where(p => p.deleted) .Select(n => CreateNote(web, conn, n, authToken, cfg, dat)) .ToList(); syncresult.saved_notes = result .saved_items .Where(p => p.content_type.ToLower() == "note") .Select(n => CreateNote(web, conn, n, authToken, cfg, dat)) .ToList(); syncresult.conflict_notes = result .unsaved .Where(p => p.item.content_type.ToLower() == "note") .Where(p => p.error.tag == "sync_conflict") .Select(n => CreateNote(web, conn, n.item, authToken, cfg, dat)) .ToList(); syncresult.error_notes = result .unsaved .Where(p => p.item.content_type.ToLower() == "note") .Where(p => p.error.tag != "sync_conflict") .Select(n => CreateNote(web, conn, n.item, authToken, cfg, dat)) .ToList(); syncresult.retrieved_notes.AddRange(GetMissingNoteUpdates(syncresult.retrieved_tags.Concat(syncresult.saved_tags), dat.Tags, allNotes, syncresult.retrieved_notes)); return(syncresult); }
public static SyncResult Sync(ISimpleJsonRest web, StandardNoteConnection conn, APIResultAuthorize authToken, StandardNoteConfig cfg, StandardNoteData dat, List <StandardFileNote> allNotes, List <StandardFileNote> notesUpload, List <StandardFileNote> notesDelete, List <StandardFileTag> tagsDelete) { APIBodySync d = new APIBodySync(); d.cursor_token = null; d.sync_token = string.IsNullOrWhiteSpace(dat.SyncToken) ? null : dat.SyncToken; d.limit = 150; d.items = new List <APIBodyItem>(); var allTags = dat.Tags.ToList(); // Upload new notes foreach (var mvNote in notesUpload) { PrepareForUpload(web, d, mvNote, allTags, authToken, cfg, false); } // Delete deleted notes foreach (var rmNote in notesDelete) { PrepareForUpload(web, d, rmNote, allTags, authToken, cfg, true); } // Update references on tags (from changed notes) foreach (var upTag in notesUpload.SelectMany(n => n.InternalTags).Concat(notesDelete.SelectMany(n => n.InternalTags)).Except(tagsDelete)) { PrepareForUpload(web, d, upTag, allNotes, authToken, cfg, false); } // Remove unused tags if (cfg.RemEmptyTags) { foreach (var rmTag in tagsDelete) { PrepareForUpload(web, d, rmTag, allNotes, authToken, cfg, true); } } var result = GetCursorResult(web, dat, d); var syncresult = new SyncResult(); syncresult.retrieved_tags = result .retrieved_items .Where(p => p.content_type.ToLower() == "tag") .Where(p => !p.deleted) .Select(n => CreateTag(web, n, authToken)) .ToList(); syncresult.deleted_tags = result .retrieved_items .Where(p => p.content_type.ToLower() == "tag") .Where(p => p.deleted) .Select(n => CreateTag(web, n, authToken)) .ToList(); syncresult.saved_tags = result .saved_items .Where(p => p.content_type.ToLower() == "tag") .Select(n => CreateTag(web, n, authToken)) .ToList(); syncresult.unsaved_tags = result .unsaved .Where(p => p.item.content_type.ToLower() == "tag") .Select(n => CreateTag(web, n.item, authToken)) .ToList(); dat.UpdateTags(syncresult.retrieved_tags, syncresult.saved_tags, syncresult.unsaved_tags, syncresult.deleted_tags); syncresult.retrieved_notes = result .retrieved_items .Where(p => p.content_type.ToLower() == "note") .Where(p => !p.deleted) .Select(n => CreateNote(web, conn, n, authToken, cfg, dat)) .ToList(); syncresult.deleted_notes = result .retrieved_items .Where(p => p.content_type.ToLower() == "note") .Where(p => p.deleted) .Select(n => CreateNote(web, conn, n, authToken, cfg, dat)) .ToList(); syncresult.saved_notes = result .saved_items .Where(p => p.content_type.ToLower() == "note") .Select(n => CreateNote(web, conn, n, authToken, cfg, dat)) .ToList(); syncresult.conflict_notes = result .unsaved .Where(p => p.item.content_type.ToLower() == "note") .Where(p => p.error.tag == "sync_conflict") .Select(n => CreateNote(web, conn, n.item, authToken, cfg, dat)) .ToList(); syncresult.error_notes = result .unsaved .Where(p => p.item.content_type.ToLower() == "note") .Where(p => p.error.tag != "sync_conflict") .Select(n => CreateNote(web, conn, n.item, authToken, cfg, dat)) .ToList(); return(syncresult); }
private static StandardFileNote CreateNote(ISimpleJsonRest web, StandardNoteConnection conn, APIResultItem encNote, APIResultAuthorize authToken, StandardNoteConfig cfg, StandardNoteData dat) { if (encNote.deleted) { var nd = new StandardFileNote(encNote.uuid, cfg, conn.HConfig) { CreationDate = encNote.created_at, AuthHash = encNote.auth_hash, ContentVersion = StandardNoteCrypt.GetSchemaVersion(encNote.content), }; nd.RawModificationDate = encNote.updated_at; return(nd); } ContentNote content; string appDataContentString; try { var contentJson = StandardNoteCrypt.DecryptContent(encNote.content, encNote.enc_item_key, encNote.auth_hash, authToken.masterkey, authToken.masterauthkey); Logger.Debug( StandardNotePlugin.Name, $"DecryptContent of note {encNote.uuid:B}", $"[content]:\r\n{encNote.content}\r\n" + $"[enc_item_key]:\r\n{encNote.enc_item_key}\r\n" + $"[auth_hash]:\r\n{encNote.auth_hash}\r\n" + $"\r\n\r\n" + $"[contentJson]:\r\n{contentJson}\r\n"); content = web.ParseJsonWithoutConverter <ContentNote>(contentJson); appDataContentString = web.ParseJsonAndGetSubJson(contentJson, "appData", string.Empty); } catch (RestException) { throw; } catch (Exception e) { throw new StandardNoteAPIException("Cannot decrypt note with local masterkey", e); } var n = new StandardFileNote(encNote.uuid, cfg, conn.HConfig); using (n.SuppressDirtyChanges()) { n.Text = StandardNoteConfig.REX_LINEBREAK.Replace(content.text, Environment.NewLine); n.InternalTitle = content.title; n.AuthHash = encNote.auth_hash; n.ContentVersion = StandardNoteCrypt.GetSchemaVersion(encNote.content); n.IsPinned = GetAppDataBool(content.appData, APPDATA_PINNED, false); n.IsLocked = GetAppDataBool(content.appData, APPDATA_LOCKED, false); n.IsArchived = GetAppDataBool(content.appData, APPDATA_ARCHIVED, false); n.IsProtected = content.@protected; n.IsHidePreview = content.hidePreview; var refTags = new List <StandardFileTagRef>(); foreach (var cref in content.references) { if (cref.content_type == "Note") { // ignore } else if (dat.Tags.Any(t => t.UUID == cref.uuid)) { refTags.Add(new StandardFileTagRef(cref.uuid, dat.Tags.First(t => t.UUID == cref.uuid).Title)); } else if (cref.content_type == "Tag") { Logger.Warn(StandardNotePlugin.Name, $"Reference to missing tag {cref.uuid} in note {encNote.uuid}"); } else { Logger.Error(StandardNotePlugin.Name, $"Downloaded note contains an unknown reference :{cref.uuid} ({cref.content_type}) in note {encNote.uuid}"); } } foreach (var tref in dat.Tags.Where(tag => tag.References.Any(tref => tref == encNote.uuid))) { refTags.Add(new StandardFileTagRef(tref.UUID, tref.Title)); } refTags = refTags.DistinctBy(t => t.UUID).ToList(); n.SetTags(refTags); n.SetReferences(content.references); n.CreationDate = encNote.created_at; n.RawModificationDate = encNote.updated_at; n.ClientUpdatedAt = GetAppDataDTO(content.appData, APPDATA_CLIENTUPDATEDAT, null); n.NoteCreationDate = GetAppDataDTO(content.appData, APPDATA_NOTECDATE, null); n.NoteModificationDate = GetAppDataDTO(content.appData, APPDATA_NOTEMDATE, null); n.TextModificationDate = GetAppDataDTO(content.appData, APPDATA_TEXTMDATE, null); n.TitleModificationDate = GetAppDataDTO(content.appData, APPDATA_TITLEMDATE, null); n.TagsModificationDate = GetAppDataDTO(content.appData, APPDATA_TAGSMDATE, null); n.PathModificationDate = GetAppDataDTO(content.appData, APPDATA_PATHMDATE, null); n.RawAppData = appDataContentString; } return(n); }