diff --git a/src/main/java/org/qortal/arbitrary/misc/Service.java b/src/main/java/org/qortal/arbitrary/misc/Service.java index 27557045..e0caa2a5 100644 --- a/src/main/java/org/qortal/arbitrary/misc/Service.java +++ b/src/main/java/org/qortal/arbitrary/misc/Service.java @@ -9,7 +9,6 @@ import org.qortal.utils.FilesystemUtils; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; @@ -20,9 +19,9 @@ import static java.util.Arrays.stream; import static java.util.stream.Collectors.toMap; public enum Service { - AUTO_UPDATE(1, false, null, false, null), - ARBITRARY_DATA(100, false, null, false, null), - QCHAT_ATTACHMENT(120, true, 1024*1024L, true, null) { + AUTO_UPDATE(1, false, null, false, false, null), + ARBITRARY_DATA(100, false, null, false, false, null), + QCHAT_ATTACHMENT(120, true, 1024*1024L, true, false, null) { @Override public ValidationResult validate(Path path) throws IOException { ValidationResult superclassResult = super.validate(path); @@ -47,11 +46,14 @@ public enum Service { return ValidationResult.OK; } }, - ATTACHMENT(130, false, null, true, null), - FILE(140, false, null, true, null), - FILES(150, false, null, false, null), - CHAIN_DATA(160, true, 239L, true, null), - WEBSITE(200, true, null, false, null) { + QCHAT_ATTACHMENT_PRIVATE(121, true, 1024*1024L, true, true, null), + ATTACHMENT(130, false, 50*1024*1024L, true, false, null), + ATTACHMENT_PRIVATE(131, true, 50*1024*1024L, true, true, null), + FILE(140, false, null, true, false, null), + FILE_PRIVATE(141, true, null, true, true, null), + FILES(150, false, null, false, false, null), + CHAIN_DATA(160, true, 239L, true, false, null), + WEBSITE(200, true, null, false, false, null) { @Override public ValidationResult validate(Path path) throws IOException { ValidationResult superclassResult = super.validate(path); @@ -73,25 +75,30 @@ public enum Service { return ValidationResult.MISSING_INDEX_FILE; } }, - GIT_REPOSITORY(300, false, null, false, null), - IMAGE(400, true, 10*1024*1024L, true, null), - THUMBNAIL(410, true, 500*1024L, true, null), - QCHAT_IMAGE(420, true, 500*1024L, true, null), - VIDEO(500, false, null, true, null), - AUDIO(600, false, null, true, null), - QCHAT_AUDIO(610, true, 10*1024*1024L, true, null), - QCHAT_VOICE(620, true, 10*1024*1024L, true, null), - VOICE(630, true, 10*1024*1024L, true, null), - PODCAST(640, false, null, true, null), - BLOG(700, false, null, false, null), - BLOG_POST(777, false, null, true, null), - BLOG_COMMENT(778, true, 500*1024L, true, null), - DOCUMENT(800, false, null, true, null), - LIST(900, true, null, true, null), - PLAYLIST(910, true, null, true, null), - APP(1000, true, 50*1024*1024L, false, null), - METADATA(1100, false, null, true, null), - JSON(1110, true, 25*1024L, true, null) { + GIT_REPOSITORY(300, false, null, false, false, null), + IMAGE(400, true, 10*1024*1024L, true, false, null), + IMAGE_PRIVATE(401, true, 10*1024*1024L, true, true, null), + THUMBNAIL(410, true, 500*1024L, true, false, null), + QCHAT_IMAGE(420, true, 500*1024L, true, false, null), + VIDEO(500, false, null, true, false, null), + VIDEO_PRIVATE(501, true, null, true, true, null), + AUDIO(600, false, null, true, false, null), + AUDIO_PRIVATE(601, true, null, true, true, null), + QCHAT_AUDIO(610, true, 10*1024*1024L, true, false, null), + QCHAT_VOICE(620, true, 10*1024*1024L, true, false, null), + VOICE(630, true, 10*1024*1024L, true, false, null), + VOICE_PRIVATE(631, true, 10*1024*1024L, true, true, null), + PODCAST(640, false, null, true, false, null), + BLOG(700, false, null, false, false, null), + BLOG_POST(777, false, null, true, false, null), + BLOG_COMMENT(778, true, 500*1024L, true, false, null), + DOCUMENT(800, false, null, true, false, null), + DOCUMENT_PRIVATE(801, true, null, true, true, null), + LIST(900, true, null, true, false, null), + PLAYLIST(910, true, null, true, false, null), + APP(1000, true, 50*1024*1024L, false, false, null), + METADATA(1100, false, null, true, false, null), + JSON(1110, true, 25*1024L, true, false, null) { @Override public ValidationResult validate(Path path) throws IOException { ValidationResult superclassResult = super.validate(path); @@ -110,7 +117,7 @@ public enum Service { } } }, - GIF_REPOSITORY(1200, true, 25*1024*1024L, false, null) { + GIF_REPOSITORY(1200, true, 25*1024*1024L, false, false, null) { @Override public ValidationResult validate(Path path) throws IOException { ValidationResult superclassResult = super.validate(path); @@ -146,27 +153,30 @@ public enum Service { return ValidationResult.OK; } }, - STORE(1300, false, null, true, null), - PRODUCT(1310, false, null, true, null), - OFFER(1330, false, null, true, null), - COUPON(1340, false, null, true, null), - CODE(1400, false, null, true, null), - PLUGIN(1410, false, null, true, null), - EXTENSION(1420, false, null, true, null), - GAME(1500, false, null, false, null), - ITEM(1510, false, null, true, null), - NFT(1600, false, null, true, null), - DATABASE(1700, false, null, false, null), - SNAPSHOT(1710, false, null, false, null), - COMMENT(1800, true, 500*1024L, true, null), - CHAIN_COMMENT(1810, true, 239L, true, null), - MAIL(1900, true, 1024*1024L, true, null), - MESSAGE(1910, true, 1024*1024L, true, null); + STORE(1300, false, null, true, false, null), + PRODUCT(1310, false, null, true, false, null), + OFFER(1330, false, null, true, false, null), + COUPON(1340, false, null, true, false, null), + CODE(1400, false, null, true, false, null), + PLUGIN(1410, false, null, true, false, null), + EXTENSION(1420, false, null, true, false, null), + GAME(1500, false, null, false, false, null), + ITEM(1510, false, null, true, false, null), + NFT(1600, false, null, true, false, null), + DATABASE(1700, false, null, false, false, null), + SNAPSHOT(1710, false, null, false, false, null), + COMMENT(1800, true, 500*1024L, true, false, null), + CHAIN_COMMENT(1810, true, 239L, true, false, null), + MAIL(1900, true, 1024*1024L, true, false, null), + MAIL_PRIVATE(1901, true, 1024*1024L, true, true, null), + MESSAGE(1910, true, 1024*1024L, true, false, null), + MESSAGE_PRIVATE(1911, true, 1024*1024L, true, true, null); public final int value; private final boolean requiresValidation; private final Long maxSize; private final boolean single; + private final boolean isPrivate; private final List requiredKeys; private static final Map map = stream(Service.values()) @@ -175,11 +185,14 @@ public enum Service { // For JSON validation private static final ObjectMapper objectMapper = new ObjectMapper(); - Service(int value, boolean requiresValidation, Long maxSize, boolean single, List requiredKeys) { + private static final String encryptedDataPrefix = "qortalEncryptedData"; + + Service(int value, boolean requiresValidation, Long maxSize, boolean single, boolean isPrivate, List requiredKeys) { this.value = value; this.requiresValidation = requiresValidation; this.maxSize = maxSize; this.single = single; + this.isPrivate = isPrivate; this.requiredKeys = requiredKeys; } @@ -203,6 +216,17 @@ public enum Service { return ValidationResult.INVALID_FILE_COUNT; } + // Validate private data for single file resources + if (this.single) { + String dataString = new String(data, StandardCharsets.UTF_8); + if (this.isPrivate && !dataString.startsWith(encryptedDataPrefix)) { + return ValidationResult.DATA_NOT_ENCRYPTED; + } + if (!this.isPrivate && dataString.startsWith(encryptedDataPrefix)) { + return ValidationResult.DATA_ENCRYPTED; + } + } + // Validate required keys if needed if (this.requiredKeys != null) { if (data == null) { @@ -221,7 +245,8 @@ public enum Service { } public boolean isValidationRequired() { - return this.requiresValidation; + // We must always validate single file resources, to ensure they are actually a single file + return this.requiresValidation || this.single; } public static Service valueOf(int value) { @@ -242,7 +267,9 @@ public enum Service { INVALID_FILE_EXTENSION(6), MISSING_DATA(7), INVALID_FILE_COUNT(8), - INVALID_CONTENT(9); + INVALID_CONTENT(9), + DATA_NOT_ENCRYPTED(10), + DATA_ENCRYPTED(10); public final int value; diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataManager.java index 567dcdd3..9284e672 100644 --- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataManager.java +++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataManager.java @@ -16,7 +16,6 @@ import org.qortal.arbitrary.misc.Service; import org.qortal.controller.Controller; import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.TransactionData; -import org.qortal.list.ResourceListManager; import org.qortal.network.Network; import org.qortal.network.Peer; import org.qortal.repository.DataException; diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java index 940b33a9..45960a25 100644 --- a/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java +++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java @@ -436,4 +436,71 @@ public class ArbitraryServiceTests extends Common { assertEquals(ValidationResult.INVALID_FILE_COUNT, service.validate(path)); } + @Test + public void testValidPrivateData() throws IOException { + String dataString = "qortalEncryptedDatabMx4fELNTV+ifJxmv4+GcuOIJOTo+3qAvbWKNY2L1rfla5UBoEcoxbtjgZ9G7FLPb8V/Qfr0bfKWfvMmN06U/pgUdLuv2mGL2V0D3qYd1011MUzGdNG1qERjaCDz8GAi63+KnHHjfMtPgYt6bcqjs4CNV+ZZ4dIt3xxHYyVEBNc="; + + // Write the data a single file in a temp path + Path path = Files.createTempDirectory("testValidPrivateData"); + Path filePath = Paths.get(path.toString(), "test"); + filePath.toFile().deleteOnExit(); + + BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toFile())); + writer.write(dataString); + writer.close(); + + Service service = Service.FILE_PRIVATE; + assertTrue(service.isValidationRequired()); + + assertEquals(ValidationResult.OK, service.validate(filePath)); + } + + @Test + public void testEncryptedData() throws IOException { + String dataString = "qortalEncryptedDatabMx4fELNTV+ifJxmv4+GcuOIJOTo+3qAvbWKNY2L1rfla5UBoEcoxbtjgZ9G7FLPb8V/Qfr0bfKWfvMmN06U/pgUdLuv2mGL2V0D3qYd1011MUzGdNG1qERjaCDz8GAi63+KnHHjfMtPgYt6bcqjs4CNV+ZZ4dIt3xxHYyVEBNc="; + + // Write the data a single file in a temp path + Path path = Files.createTempDirectory("testValidPrivateData"); + Path filePath = Paths.get(path.toString(), "test"); + filePath.toFile().deleteOnExit(); + + BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toFile())); + writer.write(dataString); + writer.close(); + + // Validate a private service + Service service = Service.FILE_PRIVATE; + assertTrue(service.isValidationRequired()); + assertEquals(ValidationResult.OK, service.validate(filePath)); + + // Validate a regular service + service = Service.FILE; + assertTrue(service.isValidationRequired()); + assertEquals(ValidationResult.DATA_ENCRYPTED, service.validate(filePath)); + } + + @Test + public void testPlainTextData() throws IOException { + String dataString = "plaintext"; + + // Write the data a single file in a temp path + Path path = Files.createTempDirectory("testInvalidPrivateData"); + Path filePath = Paths.get(path.toString(), "test"); + filePath.toFile().deleteOnExit(); + + BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toFile())); + writer.write(dataString); + writer.close(); + + // Validate a private service + Service service = Service.FILE_PRIVATE; + assertTrue(service.isValidationRequired()); + assertEquals(ValidationResult.DATA_NOT_ENCRYPTED, service.validate(filePath)); + + // Validate a regular service + service = Service.FILE; + assertTrue(service.isValidationRequired()); + assertEquals(ValidationResult.OK, service.validate(filePath)); + } + } \ No newline at end of file