diff --git a/Notesnook.API/Authorization/InboxApiKeyAuthenticationHandler.cs b/Notesnook.API/Authorization/InboxApiKeyAuthenticationHandler.cs
index 66c3899..6147871 100644
--- a/Notesnook.API/Authorization/InboxApiKeyAuthenticationHandler.cs
+++ b/Notesnook.API/Authorization/InboxApiKeyAuthenticationHandler.cs
@@ -78,7 +78,7 @@ namespace Notesnook.API.Authorization
return AuthenticateResult.Fail("API key has expired");
}
- inboxApiKey.LastUsedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ inboxApiKey.LastUsedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
await _inboxApiKeyRepository.UpsertAsync(inboxApiKey, k => k.Key == apiKey);
var claims = new[]
diff --git a/Notesnook.API/Controllers/InboxController.cs b/Notesnook.API/Controllers/InboxController.cs
index b15c9bf..1d0053e 100644
--- a/Notesnook.API/Controllers/InboxController.cs
+++ b/Notesnook.API/Controllers/InboxController.cs
@@ -151,34 +151,18 @@ namespace Notesnook.API.Controllers
var userId = User.GetUserId();
try
{
- if (request.Key.Algorithm != Algorithms.XSAL_X25519_7)
+ if (string.IsNullOrWhiteSpace(request.Cipher))
{
- return BadRequest(new { error = $"Only {Algorithms.XSAL_X25519_7} is supported for inbox item password." });
+ return BadRequest(new { error = "Inbox item is required." });
}
- if (string.IsNullOrWhiteSpace(request.Key.Cipher))
+ if (string.IsNullOrWhiteSpace(request.Algorithm))
{
- return BadRequest(new { error = "Inbox item password cipher is required." });
- }
- if (request.Key.Length <= 0)
- {
- return BadRequest(new { error = "Valid inbox item password length is required." });
- }
- if (request.Algorithm != Algorithms.Default)
- {
- return BadRequest(new { error = $"Only {Algorithms.Default} is supported for inbox item." });
+ return BadRequest(new { error = "Inbox item algorithm is required." });
}
if (request.Version <= 0)
{
return BadRequest(new { error = "Valid inbox item version is required." });
}
- if (string.IsNullOrWhiteSpace(request.Cipher) || string.IsNullOrWhiteSpace(request.IV))
- {
- return BadRequest(new { error = "Inbox item cipher and iv is required." });
- }
- if (request.Length <= 0)
- {
- return BadRequest(new { error = "Valid inbox item length is required." });
- }
request.UserId = userId;
request.ItemId = ObjectId.GenerateNewId().ToString();
diff --git a/Notesnook.API/Hubs/SyncV2Hub.cs b/Notesnook.API/Hubs/SyncV2Hub.cs
index 3215718..f81e7cd 100644
--- a/Notesnook.API/Hubs/SyncV2Hub.cs
+++ b/Notesnook.API/Hubs/SyncV2Hub.cs
@@ -132,13 +132,19 @@ namespace Notesnook.API.Hubs
var stopwatch = Stopwatch.StartNew();
try
{
-
var UpsertItems = UpsertActionsMap[pushItem.Type] ?? throw new Exception($"Invalid item type: {pushItem.Type}.");
UpsertItems(pushItem.Items, userId, 1);
if (!await unit.Commit()) return 0;
await SyncDeviceService.AddIdsToOtherDevicesAsync(userId, deviceId, pushItem.Items.Select((i) => new ItemKey(i.ItemId, pushItem.Type)));
+
+ // we need to delete the inbox items from the inbox collection
+ // after syncing to prevent them from being sent again in the
+ // next fetch.
+ var itemIds = pushItem.Items.Select(i => i.ItemId).ToList();
+ await Repositories.InboxItems.DeleteManyAsync(i => i.UserId == userId && itemIds.Contains(i.ItemId));
+
return 1;
}
finally
diff --git a/Notesnook.API/Models/Algorithms.cs b/Notesnook.API/Models/Algorithms.cs
index 91d4e2a..cea365a 100644
--- a/Notesnook.API/Models/Algorithms.cs
+++ b/Notesnook.API/Models/Algorithms.cs
@@ -22,6 +22,5 @@ namespace Notesnook.API.Models
public class Algorithms
{
public static string Default => "xcha-argon2i13-7";
- public static string XSAL_X25519_7 => "xsal-x25519-7";
}
}
\ No newline at end of file
diff --git a/Notesnook.API/Models/InboxSyncItem.cs b/Notesnook.API/Models/InboxSyncItem.cs
index 9948714..b4d9119 100644
--- a/Notesnook.API/Models/InboxSyncItem.cs
+++ b/Notesnook.API/Models/InboxSyncItem.cs
@@ -20,46 +20,64 @@ along with this program. If not, see .
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
namespace Notesnook.API.Models
{
[MessagePack.MessagePackObject]
- public class InboxSyncItem : SyncItem
+ public class InboxSyncItem
{
- [DataMember(Name = "key")]
- [JsonPropertyName("key")]
- [MessagePack.Key("key")]
- [Required]
- public required EncryptedKey Key { get; set; }
-
- [DataMember(Name = "salt")]
- [JsonPropertyName("salt")]
- [MessagePack.Key("salt")]
- [Required]
- public required string Salt { get; set; }
- }
-
- [MessagePack.MessagePackObject]
- public class EncryptedKey
- {
- [DataMember(Name = "alg")]
- [JsonPropertyName("alg")]
- [MessagePack.Key("alg")]
- [Required]
- public required string Algorithm { get; set; }
-
[DataMember(Name = "cipher")]
[JsonPropertyName("cipher")]
[MessagePack.Key("cipher")]
[Required]
- public required string Cipher { get; set; }
+ public string Cipher
+ {
+ get; set;
+ }
- [JsonPropertyName("length")]
- [DataMember(Name = "length")]
- [MessagePack.Key("length")]
+ [DataMember(Name = "userId")]
+ [JsonPropertyName("userId")]
+ [MessagePack.Key("userId")]
+ public string? UserId
+ {
+ get; set;
+ }
+
+ [DataMember(Name = "id")]
+ [JsonPropertyName("id")]
+ [MessagePack.Key("id")]
+ public string? ItemId
+ {
+ get; set;
+ }
+
+ [BsonId]
+ [BsonIgnoreIfDefault]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [JsonIgnore]
+ [MessagePack.IgnoreMember]
+ public ObjectId Id
+ {
+ get; set;
+ }
+
+ [JsonPropertyName("v")]
+ [DataMember(Name = "v")]
+ [MessagePack.Key("v")]
[Required]
- public long Length
+ public double Version
+ {
+ get; set;
+ }
+
+ [JsonPropertyName("alg")]
+ [DataMember(Name = "alg")]
+ [MessagePack.Key("alg")]
+ [Required]
+ public string Algorithm
{
get; set;
}
diff --git a/Notesnook.API/Repositories/SyncItemsRepository.cs b/Notesnook.API/Repositories/SyncItemsRepository.cs
index ba5e0fb..ed62fc6 100644
--- a/Notesnook.API/Repositories/SyncItemsRepository.cs
+++ b/Notesnook.API/Repositories/SyncItemsRepository.cs
@@ -49,7 +49,7 @@ namespace Notesnook.API.Repositories
this.logger = logger;
}
- private readonly List ALGORITHMS = [Algorithms.Default, Algorithms.XSAL_X25519_7];
+ private readonly List ALGORITHMS = [Algorithms.Default];
private bool IsValidAlgorithm(string algorithm)
{
return ALGORITHMS.Contains(algorithm);
diff --git a/Notesnook.API/Services/UserService.cs b/Notesnook.API/Services/UserService.cs
index da4b9b7..fdd1841 100644
--- a/Notesnook.API/Services/UserService.cs
+++ b/Notesnook.API/Services/UserService.cs
@@ -184,6 +184,8 @@ namespace Notesnook.API.Services
};
await Repositories.InboxApiKey.InsertAsync(defaultInboxKey);
}
+
+ await Repositories.InboxItems.DeleteManyAsync(t => t.UserId == userId);
}
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
diff --git a/Notesnook.Inbox.API/bun.lock b/Notesnook.Inbox.API/bun.lock
index 22dbdc6..7a9a2d0 100644
--- a/Notesnook.Inbox.API/bun.lock
+++ b/Notesnook.Inbox.API/bun.lock
@@ -6,7 +6,7 @@
"dependencies": {
"express": "^5.1.0",
"express-rate-limit": "^8.1.0",
- "libsodium-wrappers-sumo": "^0.7.15",
+ "openpgp": "^6.2.2",
"zod": "^4.1.9",
},
"devDependencies": {
@@ -116,10 +116,6 @@
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
- "libsodium-sumo": ["libsodium-sumo@0.7.15", "", {}, "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw=="],
-
- "libsodium-wrappers-sumo": ["libsodium-wrappers-sumo@0.7.15", "", { "dependencies": { "libsodium-sumo": "^0.7.15" } }, "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA=="],
-
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@@ -140,6 +136,8 @@
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+ "openpgp": ["openpgp@6.2.2", "", {}, "sha512-P/dyEqQ3gfwOCo+xsqffzXjmUhGn4AZTOJ1LCcN21S23vAk+EAvMJOQTsb/C8krL6GjOSBxqGYckhik7+hneNw=="],
+
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
diff --git a/Notesnook.Inbox.API/package.json b/Notesnook.Inbox.API/package.json
index 2195ffa..ff77763 100644
--- a/Notesnook.Inbox.API/package.json
+++ b/Notesnook.Inbox.API/package.json
@@ -22,7 +22,7 @@
"dependencies": {
"express": "^5.1.0",
"express-rate-limit": "^8.1.0",
- "libsodium-wrappers-sumo": "^0.7.15",
+ "openpgp": "^6.2.2",
"zod": "^4.1.9"
},
"devDependencies": {
diff --git a/Notesnook.Inbox.API/src/index.ts b/Notesnook.Inbox.API/src/index.ts
index bf3f406..c48affe 100644
--- a/Notesnook.Inbox.API/src/index.ts
+++ b/Notesnook.Inbox.API/src/index.ts
@@ -1,15 +1,13 @@
import express from "express";
-import _sodium, { base64_variants } from "libsodium-wrappers-sumo";
import { z } from "zod";
import { rateLimit } from "express-rate-limit";
+import * as openpgp from "openpgp";
const NOTESNOOK_API_SERVER_URL = process.env.NOTESNOOK_API_SERVER_URL;
if (!NOTESNOOK_API_SERVER_URL) {
throw new Error("NOTESNOOK_API_SERVER_URL is not defined");
}
-let sodium: typeof _sodium;
-
const RawInboxItemSchema = z.object({
title: z.string().min(1, "Title is required"),
pinned: z.boolean().optional(),
@@ -31,62 +29,25 @@ const RawInboxItemSchema = z.object({
interface EncryptedInboxItem {
v: 1;
- key: Omit;
- iv: string;
- alg: string;
cipher: string;
- length: number;
- salt: string;
+ alg: string;
}
-function encrypt(rawData: string, publicKey: string): EncryptedInboxItem {
- try {
- const password = sodium.crypto_aead_xchacha20poly1305_ietf_keygen();
- const saltBytes = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
- const key = sodium.crypto_pwhash(
- sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
- password,
- saltBytes,
- 3, // operations limit
- 1024 * 1024 * 8, // memory limit (8MB)
- sodium.crypto_pwhash_ALG_ARGON2I13
- );
- const nonce = sodium.randombytes_buf(
- sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
- );
- const data = sodium.from_string(rawData);
- const cipher = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
- data,
- null,
- null,
- nonce,
- key
- );
- const inboxPublicKey = sodium.from_base64(
- publicKey,
- base64_variants.URLSAFE_NO_PADDING
- );
- const encryptedKey = sodium.crypto_box_seal(key, inboxPublicKey);
-
- return {
- v: 1,
- key: {
- cipher: sodium.to_base64(
- encryptedKey,
- base64_variants.URLSAFE_NO_PADDING
- ),
- alg: `xsal-x25519-${base64_variants.URLSAFE_NO_PADDING}`,
- length: password.length,
- },
- iv: sodium.to_base64(nonce, base64_variants.URLSAFE_NO_PADDING),
- alg: `xcha-argon2i13-${base64_variants.URLSAFE_NO_PADDING}`,
- cipher: sodium.to_base64(cipher, base64_variants.URLSAFE_NO_PADDING),
- length: data.length,
- salt: sodium.to_base64(saltBytes, base64_variants.URLSAFE_NO_PADDING),
- };
- } catch (error) {
- throw new Error(`encryption failed: ${error}`);
- }
+async function encrypt(
+ rawData: string,
+ rawPublicKey: string
+): Promise {
+ const publicKey = await openpgp.readKey({ armoredKey: rawPublicKey });
+ const message = await openpgp.createMessage({ text: rawData });
+ const encrypted = await openpgp.encrypt({
+ message,
+ encryptionKeys: publicKey,
+ });
+ return {
+ v: 1,
+ cipher: encrypted,
+ alg: "pgp-aes256",
+ };
}
async function getInboxPublicEncryptionKey(apiKey: string) {
@@ -154,7 +115,7 @@ app.post("/inbox", async (req, res) => {
});
}
- const encryptedItem = encrypt(
+ const encryptedItem = await encrypt(
JSON.stringify(validationResult.data),
inboxPublicKey
);
@@ -180,14 +141,9 @@ app.post("/inbox", async (req, res) => {
}
});
-(async () => {
- await _sodium.ready;
- sodium = _sodium;
-
- const PORT = Number(process.env.PORT || "5181");
- app.listen(PORT, () => {
- console.log(`📫 notesnook inbox api server running on port ${PORT}`);
- });
-})();
+const PORT = Number(process.env.PORT || "5181");
+app.listen(PORT, () => {
+ console.log(`📫 notesnook inbox api server running on port ${PORT}`);
+});
export default app;