From 26c0b17af78ec1d66a2ca912b492506360af651c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 22 Apr 2026 15:42:04 +0200 Subject: [PATCH 1/5] wip Signed-off-by: alperozturk96 --- .../android/utils/EncryptionUtilsV2IT.kt | 268 ++++++++++++------ .../android/utils/EncryptionUtilsV2.kt | 164 ++--------- 2 files changed, 206 insertions(+), 226 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt index 3ab05fdeaf0e..6a0e8f140c87 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt @@ -25,6 +25,7 @@ import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFi import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.util.EncryptionTestIT import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue import org.junit.Assert.assertNotEquals import org.junit.Test @@ -404,11 +405,9 @@ class EncryptionUtilsV2IT : EncryptionIT() { arbitraryDataProvider ) - val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encrypted) - - encryptionUtilsV2.verifyMetadata(encrypted, metadataFile, 0, signature) + encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 0) - assertTrue(true) // if we reach this, test is successful + assertTrue(true) } private fun generateDecryptedFileV1(): com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile = @@ -641,76 +640,172 @@ class EncryptionUtilsV2IT : EncryptionIT() { assertEquals("this is a test.\n", gunzip) } -// @Test -// fun validate() { -// // ALEX -// val metadata1 = """{ -// "metadata": { -// "authenticationTag": "zMozev5R09UopLrq7Je1lw==", -// "ciphertext": "j0OBtUrEt4IveGiexjmGK7eKEaWrY70ZkteA5KxHDaZT/t2wwGy9j2FPQGpqXnW6OO3iAYPNgwFikI1smnfNvqdxzVDvhavl/IXa9Kg2niWyqK3D9zpz0YD6mDvl0XsOgTNVyGXNVREdWgzGEERCQoyHI1xowt/swe3KCXw+lf+XPF/t1PfHv0DiDVk70AeWGpPPPu6yggAIxB4Az6PEZhaQWweTC0an48l2FHj2MtB2PiMHtW2v7RMuE8Al3PtE4gOA8CMFrB+Npy6rKcFCXOgTZm5bp7q+J1qkhBDbiBYtvdsYujJ52Xa5SifTpEhGeWWLFnLLgPAQ8o6bXcWOyCoYfLfp4Jpft/Y7H8qzHbPewNSyD6maEv+xljjfU7hxibbszz5A4JjMdQy2BDGoTmJx7Mas+g6l6ZuHLVbdmgQOvD3waJBy6rOg0euux0Cn4bB4bIFEF2KvbhdGbY1Uiq9DYa7kEmSEnlcAYaHyroTkDg4ew7ER0vIBBMzKM3r+UdPVKKS66uyXtZc=", -// "nonce": "W+lxQJeGq7XAJiGfcDohkg==" -// }, -// "users": [{ -// "certificate": "-----BEGIN CERTIFICATE-----\nMIIDkDCCAnigAwIBAgIBADANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJERTEb\nMBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx\nEjAQBgNVBAoMCU5leHRjbG91ZDENMAsGA1UEAwwEam9objAeFw0yMzA3MTQwNzM0\nNTZaFw00MzA3MDkwNzM0NTZaMGExCzAJBgNVBAYTAkRFMRswGQYDVQQIDBJCYWRl\nbi1XdWVydHRlbWJlcmcxEjAQBgNVBAcMCVN0dXR0Z2FydDESMBAGA1UECgwJTmV4\ndGNsb3VkMQ0wCwYDVQQDDARqb2huMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7j3Er5YahJT0LAnSRLhpqbRo+E1AVnt98rvp3DmEfBHNzNB+DS9IBDkS\nSXM/YtfAci6Tcw8ujVBjrZX/WEmrf8ynQHxYmSaJSnP8uAT306/MceZpdpruEc9/\nS10a7vp54Zbld4NYdmfS71oVFVKgM7c/Vthx+rgu48fuxzbWAvVYLFcx47hz0DJT\nnjz2Za/R68uXpxfz7J9uEXYiqsAs/FobDsLZluT3RyywVRwKBed1EZxUeLIJiyxp\nUthhGfIb8b3Vf9jZoUVi3m5gmc4spJQHvYAkfZYHzd9ras8jBu1abQRxcu2CYnVo\n6Y0mTxhKhQS/n5gjv3ExiQF3wp/XYwIDAQABo1MwUTAdBgNVHQ4EFgQUmTeILVuB\ntv70fTGkXWGAueDp5kAwHwYDVR0jBBgwFoAUmTeILVuBtv70fTGkXWGAueDp5kAw\nDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAyVtq9XAvW7nxSW/8\nhp30z6xbzGiuviXhy/Jo91VEa8IRsWCCn3OmDFiVduTEowx76tf8clJP0gk7Pozi\n6dg/7Fin+FqQGXfCk8bLAh9gXKAikQ2GK8yRN3slRFwYC2mm23HrLdKXZHUqJcpB\nMz2zsSrOGPj1YsYOl/U8FU6KA7Yj7U3q7kDMYTAgzUPZAH+d1DISGWpZsMa0RYid\nvigCCLByiccmS/Co4Sb1esF58H+YtV5+nFBRwx881U2g2TgDKF1lPMK/y3d8B8mh\nUtW+lFxRpvyNUDpsMjOErOrtNFEYbgoUJLtqwBMmyGR+nmmh6xna331QWcRAmw0P\nnDO4ew==\n-----END CERTIFICATE-----\n", -// "encryptedMetadataKey": "HVT49bYmwXbGs/dJ2avgU9unrKnPf03MYUI5ZysSR1Bz5pqz64gzH2GBAuUJ+Q4VmHtEfcMaWW7VXgzfCQv5xLBrk+RSgcLOKnlIya8jaDlfttWxbe8jJK+/0+QVPOc6ycA/t5HNCPg09hzj+gnb2L89UHxL5accZD0iEzb5cQbGrc/N6GthjgGrgFKtFf0HhDVplUr+DL9aTyKuKLBPjrjuZbv8M6ZfXO93mOMwSZH3c3rwDUHb/KEaTR/Og4pWQmrqr1VxGLqeV/+GKWhzMYThrOZAUz+5gsbckU2M5V9i+ph0yBI5BjOZVhNuDwW8yP8WtyRJwQc+UBRei/RGBQ==", -// "userId": "john" -// }], -// "version": "2" -// } -// -// """ -// -// val signature1 = -// "ewogICAgIm1ldGFkYXRhIjogewogICAgICAgICJhdXRoZW50aWNhdGlvblRhZyI6ICJ6TW96ZXY1UjA5VW9wTHJxN0plMWx3PT0iLAogICAgICAgICJjaXBoZXJ0ZXh0IjogImowT0J0VXJFdDRJdmVHaWV4am1HSzdlS0VhV3JZNzBaa3RlQTVLeEhEYVpUL3Qyd3dHeTlqMkZQUUdwcVhuVzZPTzNpQVlQTmd3RmlrSTFzbW5mTnZxZHh6VkR2aGF2bC9JWGE5S2cybmlXeXFLM0Q5enB6MFlENm1EdmwwWHNPZ1ROVnlHWE5WUkVkV2d6R0VFUkNRb3lISTF4b3d0L3N3ZTNLQ1h3K2xmK1hQRi90MVBmSHYwRGlEVms3MEFlV0dwUFBQdTZ5Z2dBSXhCNEF6NlBFWmhhUVd3ZVRDMGFuNDhsMkZIajJNdEIyUGlNSHRXMnY3Uk11RThBbDNQdEU0Z09BOENNRnJCK05weTZyS2NGQ1hPZ1RabTVicDdxK0oxcWtoQkRiaUJZdHZkc1l1ako1MlhhNVNpZlRwRWhHZVdXTEZuTExnUEFROG82YlhjV095Q29ZZkxmcDRKcGZ0L1k3SDhxekhiUGV3TlN5RDZtYUV2K3hsampmVTdoeGliYnN6ejVBNEpqTWRReTJCREdvVG1KeDdNYXMrZzZsNlp1SExWYmRtZ1FPdkQzd2FKQnk2ck9nMGV1dXgwQ240YkI0YklGRUYyS3ZiaGRHYlkxVWlxOURZYTdrRW1TRW5sY0FZYUh5cm9Ua0RnNGV3N0VSMHZJQkJNektNM3IrVWRQVktLUzY2dXlYdFpjPSIsCiAgICAgICAgIm5vbmNlIjogIlcrbHhRSmVHcTdYQUppR2ZjRG9oa2c9PSIKICAgIH0sCiAgICAidXNlcnMiOiB7CiAgICAgICAgImNlcnRpZmljYXRlIjogIi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLVxuTUlJRGtEQ0NBbmlnQXdJQkFnSUJBREFOQmdrcWhraUc5dzBCQVFVRkFEQmhNUXN3Q1FZRFZRUUdFd0pFUlRFYlxuTUJrR0ExVUVDQXdTUW1Ga1pXNHRWM1ZsY25SMFpXMWlaWEpuTVJJd0VBWURWUVFIREFsVGRIVjBkR2RoY25ReFxuRWpBUUJnTlZCQW9NQ1U1bGVIUmpiRzkxWkRFTk1Bc0dBMVVFQXd3RWFtOW9iakFlRncweU16QTNNVFF3TnpNMFxuTlRaYUZ3MDBNekEzTURrd056TTBOVFphTUdFeEN6QUpCZ05WQkFZVEFrUkZNUnN3R1FZRFZRUUlEQkpDWVdSbFxuYmkxWGRXVnlkSFJsYldKbGNtY3hFakFRQmdOVkJBY01DVk4wZFhSMFoyRnlkREVTTUJBR0ExVUVDZ3dKVG1WNFxuZEdOc2IzVmtNUTB3Q3dZRFZRUUREQVJxYjJodU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQlxuQ2dLQ0FRRUE3ajNFcjVZYWhKVDBMQW5TUkxocHFiUm8rRTFBVm50OThydnAzRG1FZkJITnpOQitEUzlJQkRrU1xuU1hNL1l0ZkFjaTZUY3c4dWpWQmpyWlgvV0VtcmY4eW5RSHhZbVNhSlNuUDh1QVQzMDYvTWNlWnBkcHJ1RWM5L1xuUzEwYTd2cDU0WmJsZDROWWRtZlM3MW9WRlZLZ003Yy9WdGh4K3JndTQ4ZnV4emJXQXZWWUxGY3g0N2h6MERKVFxubmp6MlphL1I2OHVYcHhmejdKOXVFWFlpcXNBcy9Gb2JEc0xabHVUM1J5eXdWUndLQmVkMUVaeFVlTElKaXl4cFxuVXRoaEdmSWI4YjNWZjlqWm9VVmkzbTVnbWM0c3BKUUh2WUFrZlpZSHpkOXJhczhqQnUxYWJRUnhjdTJDWW5Wb1xuNlkwbVR4aEtoUVMvbjVnanYzRXhpUUYzd3AvWFl3SURBUUFCbzFNd1VUQWRCZ05WSFE0RUZnUVVtVGVJTFZ1QlxudHY3MGZUR2tYV0dBdWVEcDVrQXdId1lEVlIwakJCZ3dGb0FVbVRlSUxWdUJ0djcwZlRHa1hXR0F1ZURwNWtBd1xuRHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBeVZ0cTlYQXZXN254U1cvOFxuaHAzMHo2eGJ6R2l1dmlYaHkvSm85MVZFYThJUnNXQ0NuM09tREZpVmR1VEVvd3g3NnRmOGNsSlAwZ2s3UG96aVxuNmRnLzdGaW4rRnFRR1hmQ2s4YkxBaDlnWEtBaWtRMkdLOHlSTjNzbFJGd1lDMm1tMjNIckxkS1haSFVxSmNwQlxuTXoyenNTck9HUGoxWXNZT2wvVThGVTZLQTdZajdVM3E3a0RNWVRBZ3pVUFpBSCtkMURJU0dXcFpzTWEwUllpZFxudmlnQ0NMQnlpY2NtUy9DbzRTYjFlc0Y1OEgrWXRWNStuRkJSd3g4ODFVMmcyVGdES0YxbFBNSy95M2Q4QjhtaFxuVXRXK2xGeFJwdnlOVURwc01qT0VyT3J0TkZFWWJnb1VKTHRxd0JNbXlHUitubW1oNnhuYTMzMVFXY1JBbXcwUFxubkRPNGV3PT1cbi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS1cbiIsCiAgICAgICAgImVuY3J5cHRlZE1ldGFkYXRhS2V5IjogIkhWVDQ5Ylltd1hiR3MvZEoyYXZnVTl1bnJLblBmMDNNWVVJNVp5c1NSMUJ6NXBxejY0Z3pIMkdCQXVVSitRNFZtSHRFZmNNYVdXN1ZYZ3pmQ1F2NXhMQnJrK1JTZ2NMT0tubEl5YThqYURsZnR0V3hiZThqSksrLzArUVZQT2M2eWNBL3Q1SE5DUGcwOWh6aitnbmIyTDg5VUh4TDVhY2NaRDBpRXpiNWNRYkdyYy9ONkd0aGpnR3JnRkt0RmYwSGhEVnBsVXIrREw5YVR5S3VLTEJQanJqdVpidjhNNlpmWE85M21PTXdTWkgzYzNyd0RVSGIvS0VhVFIvT2c0cFdRbXJxcjFWeEdMcWVWLytHS1doek1ZVGhyT1pBVXorNWdzYmNrVTJNNVY5aStwaDB5Qkk1QmpPWlZoTnVEd1c4eVA4V3R5Ukp3UWMrVUJSZWkvUkdCUT09IiwKICAgICAgICAidXNlcklkIjogImpvaG4iCiAgICB9LAogICAgInZlcnNpb24iOiAiMiIKfQo=" -// -// // TOBI -// val metadata = -// """{"metadata":{"authenticationTag":"qDcJnAAGtGDlHWiQMBfXgw\u003d\u003d","ciphertext":"3zUhwIgJWMB7DvrbsDaMvh8MbJdoTxL0OMPCCdYSfBt7gB+V/hwqelL1IOaLto3avhHGSebnrotF06iEP/jZwWg9hApIPTHc8B4XTOY0/kezqYyVqTyquTUZpDpqgVAheQskZZ8I4Ir0seajUkt4KtVRfzO6v8CePRrEg6uKwdYsqDcJnAAGtGDlHWiQMBfXgw\u003d\u003d|4hbOyn1ykQL+9D6SnPY3cQ\u003d\u003d","nonce":"4hbOyn1ykQL+9D6SnPY3cQ\u003d\u003d"},"users":[{"certificate":"-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIBADANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDDAJ0MTAe\nFw0yMzA3MjUwNzU3MTJaFw00MzA3MjAwNzU3MTJaMA0xCzAJBgNVBAMMAnQxMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtafHmDBcBqIu4HmMxMDW3j0S\ny+S0YaKwHnBRt85KSwcEov0B5FOLuLknoBGx4Dn3u93ilThXXxacPMHeXL7WPuAs\n21/G7vsqwvrRRnCduf+FUO/AZeDCNErzpsQ8LmTa4PUloLPUcImpSjrHwhMs9Ekv\nEbLRjbeSmSp9XvM+1fV/3jkT5jkOSnCFx5TGwGN5uHqwUir4UWXasvg253NK2XmW\nipKCDCR9TmH1baP3pNdoiChdmErT1c6E4DbBXpTw8XgP5ZbYH+qg1UQ/hC8nRJ3D\nyCcHL+dg/GYraBMhDn4w2Vvq77xNNoNWQ9cT5Ay6cJbQLBQoJQirygQFrobYRQID\nAQABo1MwUTAdBgNVHQ4EFgQUE9zCeA9/QMAtVgLxD23X6ZcodhMwHwYDVR0jBBgw\nFoAUE9zCeA9/QMAtVgLxD23X6ZcodhMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG\n9w0BAQUFAAOCAQEAZdy/YjJlvnz3FQwxp6oVtMJccpdxveEPfLzgaverhtd/vP8O\nAvDzOLgQJHmrDS91SG503eU4cYGyuNKwd77OyTnqMg+GUEmJhGfPpSVrEIdh65jv\nq61T4oqBdehevVmBq54rGiwL0DGv1DlXQlwiJZP4qni2KnOEFcnvL3gVtRnQjXQ+\nkHvlMshkK6w021EMV5NfjG2zg67wC65rLaej5f6Ssp2S7g2VtmE4aXq1bjAuEbqk\n4TiyZHLDdsJuqzyGyyOpMV7i9ucXDoaZt9cGS9hT2vRxTrSH63vKR8Xeig9+stLw\nt9ONcUqCKP7hd8rajtxM4JIIRExwD8OkgARWGg\u003d\u003d\n-----END CERTIFICATE-----\n","encryptedMetadataKey":"s4kDkkLpk1mSmXedP7huiCNC4DYmDAmA2VYGem5M8jIGPC6miVQoo4WXZrEBhdsLw7Msf5iT3A3fTaHhwsI8Jf4McsFyM9/FXT1mCEaGOEpNjbKOlJY1uPUFNOhLqUfFiBos6oBT53hWwoXWjytYvLBbXuXY5YLOysjgBh6URrgFUZAJAmcOJ6OFKgfIIthoqkQc7CQUY97VsRzAXzeYTANBc2yW1pSN51HqftvMzvewFRsJQLcu7a9NjpTdG9LiLhn5eLXOLymXEE/aaPHKXeprlXLzrdWU1xwZRJqV+to2FEiH6CQNsO4+9h5m0VjXekiNeAFrsXB5cJgUipGuzQ\u003d\u003d","userId":"t1"}],"version":"2.0"}""" -// -// val base = EncryptionUtils.encodeStringToBase64String(metadata) -// -// val signature = -// "MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBoIAwggLoMIIB0KADAgECAgEAMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNVBAMMAnQxMB4XDTIzMDcyNTA3NTcxMloXDTQzMDcyMDA3NTcxMlowDTELMAkGA1UEAwwCdDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1p8eYMFwGoi7geYzEwNbePRLL5LRhorAecFG3zkpLBwSi/QHkU4u4uSegEbHgOfe73eKVOFdfFpw8wd5cvtY+4CzbX8bu+yrC+tFGcJ25/4VQ78Bl4MI0SvOmxDwuZNrg9SWgs9RwialKOsfCEyz0SS8RstGNt5KZKn1e8z7V9X/eORPmOQ5KcIXHlMbAY3m4erBSKvhRZdqy+Dbnc0rZeZaKkoIMJH1OYfVto/ek12iIKF2YStPVzoTgNsFelPDxeA/lltgf6qDVRD+ELydEncPIJwcv52D8ZitoEyEOfjDZW+rvvE02g1ZD1xPkDLpwltAsFCglCKvKBAWuhthFAgMBAAGjUzBRMB0GA1UdDgQWBBQT3MJ4D39AwC1WAvEPbdfplyh2EzAfBgNVHSMEGDAWgBQT3MJ4D39AwC1WAvEPbdfplyh2EzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBl3L9iMmW+fPcVDDGnqhW0wlxyl3G94Q98vOBq96uG13+8/w4C8PM4uBAkeasNL3VIbnTd5ThxgbK40rB3vs7JOeoyD4ZQSYmEZ8+lJWsQh2HrmO+rrVPiioF16F69WYGrnisaLAvQMa/UOVdCXCIlk/iqeLYqc4QVye8veBW1GdCNdD6Qe+UyyGQrrDTbUQxXk1+MbbODrvALrmstp6Pl/pKynZLuDZW2YThperVuMC4RuqThOLJkcsN2wm6rPIbLI6kxXuL25xcOhpm31wZL2FPa9HFOtIfre8pHxd6KD36y0vC3041xSoIo/uF3ytqO3EzgkghETHAPw6SABFYaAAAxggHUMIIB0AIBATASMA0xCzAJBgNVBAMMAnQxAgEAMAsGCWCGSAFlAwQCAaCBljAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzA3MjgwNzMwMTJaMCsGCSqGSIb3DQEJNDEeMBwwCwYJYIZIAWUDBAIBoQ0GCSqGSIb3DQEBCwUAMC8GCSqGSIb3DQEJBDEiBCAx7RTJg7hbY5Mkzjw3f6qhX7k/J0FdVz2cL3ow0AmyYjANBgkqhkiG9w0BAQsFAASCAQAbUmb9e7eoIcPNzDSmnzbrueBzgT8YszNGEI+1YCq8XdWN4kDztvP1ZNV21VCO6BvcbfUAnXXgcX5BPeLZNsgXPj3c8TbD59GQl3oT/tIchgMsA20RdAtIwvItlZKh+X6sp0OHkRPYSk/mEYKCKPqrKdJicRWex8ItCwpDR91KSOiKJrN/+DKOGG0sVI9gjzbtrHsN8HmVKxOoNV+wwipcLsWsEmuV+wvPCQ9HJidLX9Q17Bgfc+qJg19aB6iKLWPhjgnfpKGbK5VJuQTdDWPUJ2O4G3W/iwxJ0hAJ7tks4zIATmgGzhgTWYx5LVXbKcuL04xhIOjqwedHeCSBZSSaAAAAAAAA" -// -// val metadataFile = EncryptionUtils.deserializeJSON( -// metadata, -// object : TypeToken() {} -// ) -// assertNotNull(metadataFile) -// -// val certJohnString = metadataFile.users[0].certificate -// val certJohn = EncryptionUtils.convertCertFromString(certJohnString) -// -// val t1String = """-----BEGIN CERTIFICATE----- -// MIIC6DCCAdCgAwIBAgIBADANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDDAJ0MTAe -// Fw0yMzA3MjUwNzU3MTJaFw00MzA3MjAwNzU3MTJaMA0xCzAJBgNVBAMMAnQxMIIB -// IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtafHmDBcBqIu4HmMxMDW3j0S -// y+S0YaKwHnBRt85KSwcEov0B5FOLuLknoBGx4Dn3u93ilThXXxacPMHeXL7WPuAs -// 21/G7vsqwvrRRnCduf+FUO/AZeDCNErzpsQ8LmTa4PUloLPUcImpSjrHwhMs9Ekv -// EbLRjbeSmSp9XvM+1fV/3jkT5jkOSnCFx5TGwGN5uHqwUir4UWXasvg253NK2XmW -// ipKCDCR9TmH1baP3pNdoiChdmErT1c6E4DbBXpTw8XgP5ZbYH+qg1UQ/hC8nRJ3D -// yCcHL+dg/GYraBMhDn4w2Vvq77xNNoNWQ9cT5Ay6cJbQLBQoJQirygQFrobYRQID -// AQABo1MwUTAdBgNVHQ4EFgQUE9zCeA9/QMAtVgLxD23X6ZcodhMwHwYDVR0jBBgw -// FoAUE9zCeA9/QMAtVgLxD23X6ZcodhMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG -// 9w0BAQUFAAOCAQEAZdy/YjJlvnz3FQwxp6oVtMJccpdxveEPfLzgaverhtd/vP8O -// AvDzOLgQJHmrDS91SG503eU4cYGyuNKwd77OyTnqMg+GUEmJhGfPpSVrEIdh65jv -// q61T4oqBdehevVmBq54rGiwL0DGv1DlXQlwiJZP4qni2KnOEFcnvL3gVtRnQjXQ+ -// kHvlMshkK6w021EMV5NfjG2zg67wC65rLaej5f6Ssp2S7g2VtmE4aXq1bjAuEbqk -// 4TiyZHLDdsJuqzyGyyOpMV7i9ucXDoaZt9cGS9hT2vRxTrSH63vKR8Xeig9+stLw -// t9ONcUqCKP7hd8rajtxM4JIIRExwD8OkgARWGg== -// -----END CERTIFICATE-----""" -// -// val t1cert = EncryptionUtils.convertCertFromString(t1String) -// val t1PrivateKeyKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) -// -// // val signed = encryptionUtilsV2.getMessageSignature( -// // t1cert, -// // t1PrivateKeyKey, -// // metadataFile -// // ) -// -// assertTrue(encryptionUtilsV2.verifySignedMessage(signature1, metadata1, listOf(certJohn, t1cert))) -// } + @Test + fun verifyMetadataEmptyFolder() { + val folder = OCFile("/e/") + val enc1 = MockUser("enc1", "Nextcloud") + + val metadataKey = EncryptionUtils.generateKey() + val metadata = DecryptedMetadata( + mutableListOf(), + false, + 0, + mutableMapOf(), + mutableMapOf(), + metadataKey + ) + // checksum must be present before encrypting, otherwise verifyMetadata returns false + metadata.keyChecksums.add(encryptionUtilsV2.hashMetadataKey(metadataKey)) + + val metadataFile = DecryptedFolderMetadataFile( + metadata, + mutableListOf(DecryptedUser(enc1.accountName, enc1Cert, null)), + mutableMapOf() + ) + + val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( + metadataFile, + enc1UserId, + folder, + storageManager, + client, + enc1PrivateKey, + user, + targetContext, + arbitraryDataProvider + ) + + val result = encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 0) + + assertTrue(result) + assertEquals(0, metadataFile.metadata.files.size) + assertEquals(0, metadataFile.metadata.folders.size) + assertEquals(0, metadataFile.metadata.counter) + } + + @Test + fun verifyMetadataWithOneFile() { + val folder = OCFile("/e/") + val enc1 = MockUser("enc1", "Nextcloud") + + val metadata = DecryptedMetadata( + mutableListOf(), + false, + 1, + mutableMapOf(), + mutableMapOf( + Pair( + EncryptionUtils.generateUid(), + DecryptedFile( + "document.pdf", + "application/pdf", + "initializationVector", + "authenticationTag", + "key1" + ) + ) + ), + EncryptionUtils.generateKey() + ) + metadata.keyChecksums.add(encryptionUtilsV2.hashMetadataKey(metadata.metadataKey)) + + val metadataFile = DecryptedFolderMetadataFile( + metadata, + mutableListOf(DecryptedUser(enc1.accountName, enc1Cert, null)), + mutableMapOf() + ) + + val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( + metadataFile, + enc1UserId, + folder, + storageManager, + client, + enc1PrivateKey, + user, + targetContext, + arbitraryDataProvider + ) + + val result = encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 0) + + assertTrue(result) + assertEquals(1, metadataFile.metadata.files.size) + assertEquals("document.pdf", metadataFile.metadata.files.values.first().filename) + } + + @Test + fun verifyMetadataCounterTooOld() { + val folder = OCFile("/e/") + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + + val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( + metadataFile, + enc1UserId, + folder, + storageManager, + client, + enc1PrivateKey, + user, + targetContext, + arbitraryDataProvider + ) + + // counter in metadata is 1, passing oldCounter=2 should fail + val result = encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 2) + + assertFalse(result) + } + + @Test + fun verifyMetadataInvalidSignature() { + val folder = OCFile("/e/") + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + + val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( + metadataFile, + enc1UserId, + folder, + storageManager, + client, + enc1PrivateKey, + user, + targetContext, + arbitraryDataProvider + ) + + val result = encryptionUtilsV2.verifyMetadata(enc2PrivateKey, encrypted, metadataFile, 0) + + assertFalse(result) + } + + @Test + fun verifyMetadataChecksumMismatch() { + val folder = OCFile("/e/") + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + + val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( + metadataFile, + enc1UserId, + folder, + storageManager, + client, + enc1PrivateKey, + user, + targetContext, + arbitraryDataProvider + ) + + // Replace the metadata key with a new one whose hash is NOT in keyChecksums + metadataFile.metadata.metadataKey = EncryptionUtils.generateKey() + + val result = encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 0) + + assertFalse(result) + } @Throws(Throwable::class) @Test @@ -744,22 +839,15 @@ class EncryptionUtilsV2IT : EncryptionIT() { |Rei/RGBQ==","userId": "john"}],"version": "2"} """.trimMargin() - val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) val certificateT1 = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey) val certificateEnc2 = EncryptionUtils.convertCertFromString(enc2Cert) - val signed = encryptionUtilsV2.signMessage( - certificateT1, - privateKey, - metadata - ) - val certs = listOf( certificateEnc2, certificateT1 ) - assertTrue(encryptionUtilsV2.verifySignedData(signed, certs)) + assertTrue(encryptionUtilsV2.verifySignedData(metadata, encryptionTestUtils.t1PrivateKey, certs)) } @Throws(Throwable::class) @@ -767,21 +855,14 @@ class EncryptionUtilsV2IT : EncryptionIT() { fun sign() { val sut = "randomstring123" - val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) val certificate = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey) - val signed = encryptionUtilsV2.signMessage( - certificate, - privateKey, - sut - ) - val certs = listOf( EncryptionUtils.convertCertFromString(enc2Cert), certificate ) - assertTrue(encryptionUtilsV2.verifySignedData(signed, certs)) + assertTrue(encryptionUtilsV2.verifySignedData(sut, encryptionTestUtils.t1PrivateKey, certs)) } @Test @@ -858,6 +939,9 @@ class EncryptionUtilsV2IT : EncryptionIT() { ) // V1 doesn't have decryptedMetadataKey so that we can ignore it for comparison + for (user in decryptedFolderMetadata1.users) { + user.decryptedMetadataKey = null + } for (user in decryptedFolderMetadata2.users) { user.decryptedMetadataKey = null } @@ -865,8 +949,8 @@ class EncryptionUtilsV2IT : EncryptionIT() { // compare assertTrue( EncryptionTestIT.compareJsonStrings( - EncryptionUtils.serializeJSON(decryptedFolderMetadata1), - EncryptionUtils.serializeJSON(decryptedFolderMetadata2) + EncryptionUtils.serializeJSON(decryptedFolderMetadata1, true), + EncryptionUtils.serializeJSON(decryptedFolderMetadata2, true) ) ) } diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index e87ef399d635..9cddda91ae03 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -41,8 +41,6 @@ import com.owncloud.android.lib.resources.e2ee.StoreMetadataV2RemoteOperation import com.owncloud.android.lib.resources.e2ee.UpdateMetadataV2RemoteOperation import com.owncloud.android.operations.UploadException import org.apache.commons.httpclient.HttpStatus -import org.bouncycastle.asn1.ASN1Sequence -import org.bouncycastle.asn1.cms.ContentInfo import org.bouncycastle.cert.jcajce.JcaCertStore import org.bouncycastle.cms.CMSProcessableByteArray import org.bouncycastle.cms.CMSSignedData @@ -137,25 +135,6 @@ class EncryptionUtilsV2 { encryptedUsers, mutableMapOf() ) - - // if (metadataFile.users.isEmpty()) { - // // we are in a subfolder, re-use users array - // retrieveTopMostMetadata( - // ocFile, - // storageManager, - // client - // ) - // } else { - // val encryptedUsers = metadataFile.users.map { - // encryptUser(it, metadataFile.metadata.metadataKey) - // } - // - // return EncryptedFolderMetadataFile( - // encryptedMetadata, - // encryptedUsers, - // emptyMap() - // ) - // } } @Throws(IllegalStateException::class, UploadException::class, Throwable::class) @@ -250,7 +229,7 @@ class EncryptionUtilsV2 { ) } - if (!verifyMetadata(metadataFile, decryptedFolderMetadataFile, oldCounter, signature)) { + if (!verifyMetadata(privateKey, metadataFile, decryptedFolderMetadataFile, oldCounter)) { throw IllegalStateException("Metadata is corrupt!") } @@ -679,86 +658,6 @@ class EncryptionUtilsV2 { } return decryptedFolderMetadata - - // handle filesDrops - // TODO re-add -// try { -// int filesDropCountBefore = encryptedFolderMetadata.getFiledrop().size(); -// DecryptedFolderMetadataFile decryptedFolderMetadata = new EncryptionUtilsV2().decryptFolderMetadataFile( -// encryptedFolderMetadata, -// privateKey); -// -// boolean transferredFiledrop = filesDropCountBefore > 0 && decryptedFolderMetadata.getFiles().size() == -// encryptedFolderMetadata.getFiles().size() + filesDropCountBefore; -// -// if (transferredFiledrop) { -// // lock folder, only if not already locked -// String token; -// if (existingLockToken == null) { -// token = EncryptionUtils.lockFolder(folder, client); -// } else { -// token = existingLockToken; -// } -// -// // upload metadata -// EncryptedFolderMetadataFile encryptedFolderMetadataNew = -// encryptFolderMetadata(decryptedFolderMetadata, privateKey); -// -// String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadataNew); -// -// EncryptionUtils.uploadMetadata(folder, -// serializedFolderMetadata, -// token, -// client, -// true); -// -// // unlock folder, only if not previously locked -// if (existingLockToken == null) { -// RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token); -// -// if (!unlockFolderResult.isSuccess()) { -// Log_OC.e(TAG, unlockFolderResult.getMessage()); -// -// return null; -// } -// } -// } -// -// return decryptedFolderMetadata; -// } catch (Exception e) { -// Log_OC.e(TAG, e.getMessage()); -// return null; -// } - - // TODO to check -// try { -// int filesDropCountBefore = 0; -// if (encryptedFolderMetadata.getFiledrop() != null) { -// filesDropCountBefore = encryptedFolderMetadata.getFiledrop().size(); -// } -// DecryptedFolderMetadataFile decryptedFolderMetadata = EncryptionUtils.decryptFolderMetaData( -// encryptedFolderMetadata, -// privateKey, -// arbitraryDataProvider, -// user, -// folder.getLocalId()); -// -// boolean transferredFiledrop = filesDropCountBefore > 0 && -// decryptedFolderMetadata.getFiles().size() == -// encryptedFolderMetadata.getFiles().size() + filesDropCountBefore; -// -// if (transferredFiledrop) { -// // lock folder -// String token = EncryptionUtils.lockFolder(folder, client); -// -// // upload metadata -// EncryptedFolderMetadata encryptedFolderMetadataNew = -// encryptFolderMetadata(decryptedFolderMetadata, -// publicKey, -// arbitraryDataProvider, -// user, -// folder.getLocalId()); -// } @Throws(UploadException::class) @@ -951,21 +850,20 @@ class EncryptionUtilsV2 { @Suppress("ReturnCount") fun verifyMetadata( + privateKey: String, encryptedFolderMetadataFile: EncryptedFolderMetadataFile, decryptedFolderMetadataFile: DecryptedFolderMetadataFile, - oldCounter: Long, - signature: String + oldCounter: Long ): Boolean { if (decryptedFolderMetadataFile.metadata.counter < oldCounter) { MainApp.showMessage(R.string.e2e_counter_too_old) return false } - val message = EncryptionUtils.serializeJSON(encryptedFolderMetadataFile, true) + val folderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadataFile, true) val certs = decryptedFolderMetadataFile.users.map { EncryptionUtils.convertCertFromString(it.certificate) } - val signedData = getSignedData(signature, message) - if (certs.isNotEmpty() && !verifySignedData(signedData, certs)) { + if (certs.isNotEmpty() && !verifySignedData(folderMetadata, privateKey, certs)) { MainApp.showMessage(R.string.e2e_signature_does_not_match) return false } @@ -978,31 +876,34 @@ class EncryptionUtilsV2 { return true } - private fun getSignedData(base64encodedSignature: String, message: String): CMSSignedData { - val signature = EncryptionUtils.decodeStringToBase64Bytes(base64encodedSignature) - val asn1Signature = ASN1Sequence.fromByteArray(signature) - val contentInfo = ContentInfo.getInstance(asn1Signature) - - val encodedMessage = EncryptionUtils.encodeStringToBase64String(message) - val messageData = encodedMessage.toByteArray() - val cmsProcessableByteArray = CMSProcessableByteArray(messageData) - - return CMSSignedData(cmsProcessableByteArray, contentInfo) - } - @Suppress("TooGenericExceptionCaught") - fun verifySignedData(data: CMSSignedData, certs: List): Boolean { - val signer = data.signerInfos.signers.first() as SignerInformation - val verifierBuilder = JcaSimpleSignerInfoVerifierBuilder() - - return certs.any { cert -> - runCatching { - signer.verify(verifierBuilder.build(cert.publicKey)) - }.getOrElse { - Log_OC.e(TAG, "Exception verifySignedData: $it") - false + fun verifySignedData(folderMetadata: String, privateKey: String, certs: List): Boolean { + for (cert in certs) { + try { + val privateKey = EncryptionUtils.PEMtoPrivateKey(privateKey) + val data = signMessage( + cert, + privateKey, + folderMetadata + ) + val signers = data.signerInfos.signers + if (signers.isEmpty()) { + Log_OC.e(TAG, "verifySignedData: no signers found in CMSSignedData") + continue + } + + val signer = signers.first() as SignerInformation + val verifierBuilder = JcaSimpleSignerInfoVerifierBuilder() + if (signer.verify(verifierBuilder.build(cert.publicKey))) { + return true + } + } catch (e: Exception) { + Log_OC.e(TAG, "exception verifySignedData: $e") + continue } } + + return false } private fun signMessage(cert: X509Certificate, key: PrivateKey, data: ByteArray): CMSSignedData { @@ -1089,11 +990,6 @@ class EncryptionUtilsV2 { return extractSignedString(signedMessage) } - fun getMessageSignature(cert: X509Certificate, key: PrivateKey, string: String): String { - val signedMessage = signMessage(cert, key, string) - return extractSignedString(signedMessage) - } - companion object { private val TAG = EncryptionUtils::class.java.simpleName } From 34b71fd7325e74a0cc001731617baa3bfdec3c9b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Apr 2026 08:39:53 +0200 Subject: [PATCH 2/5] add tests for upload file operation Signed-off-by: alperozturk96 --- .../java/com/owncloud/android/UploadIT.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/app/src/androidTest/java/com/owncloud/android/UploadIT.java b/app/src/androidTest/java/com/owncloud/android/UploadIT.java index 8072bb5c1805..a7557c822429 100644 --- a/app/src/androidTest/java/com/owncloud/android/UploadIT.java +++ b/app/src/androidTest/java/com/owncloud/android/UploadIT.java @@ -27,6 +27,7 @@ import com.owncloud.android.operations.RemoveFileOperation; import com.owncloud.android.operations.UploadFileOperation; import com.owncloud.android.utils.FileStorageUtils; +import com.owncloud.android.utils.MimeType; import org.junit.Before; import org.junit.Test; @@ -514,6 +515,101 @@ public void testMetadata() throws IOException, AccountUtils.AccountNotFoundExcep assertEquals(new ImageDimension(300f, 200f), ocFile.getImageDimension()); } + @Test + public void testEncryptedUploadCallsEncryptedUploadNotNormalUpload() { + OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt", + FOLDER + "nonEmpty.txt", + account.name); + + OCFile encryptedFile = new OCFile(FOLDER + "nonEmpty.txt"); + encryptedFile.setEncrypted(true); + encryptedFile.setStoragePath(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt"); + + UploadFileOperation newUpload = new UploadFileOperation( + uploadsStorageManager, + connectivityServiceMock, + powerManagementServiceMock, + user, + encryptedFile, + ocUpload, + NameCollisionPolicy.DEFAULT, + FileUploadWorker.LOCAL_BEHAVIOUR_COPY, + targetContext, + false, + false, + getStorageManager() + ); + newUpload.setRemoteFolderToBeCreated(); + newUpload.addRenameUploadListener(() -> {}); + + newUpload.execute(client); + + assertNotNull(newUpload.getDecryptedRemotePath()); + assertFalse(newUpload.getDecryptedRemotePath().isEmpty()); + assertTrue(newUpload.getFile().isEncrypted()); + } + + @Test + public void testEncryptedAncestorTriggerEncryptedUpload() { + OCFile encryptedParentFolder = new OCFile(FOLDER); + encryptedParentFolder.setMimeType(MimeType.DIRECTORY); + encryptedParentFolder.setEncrypted(true); + encryptedParentFolder.setDecryptedRemotePath(FOLDER); + encryptedParentFolder.setParentId(getStorageManager().getFileByDecryptedRemotePath("/").getFileId()); + getStorageManager().saveFile(encryptedParentFolder); + + // Reload so we get the assigned fileId back from the DB + encryptedParentFolder = getStorageManager().getFileByDecryptedRemotePath(FOLDER); + assertNotNull("Encrypted parent folder must exist in storage manager", encryptedParentFolder); + assertTrue("Parent folder must be marked encrypted", encryptedParentFolder.isEncrypted()); + + // Create the child file pointing at the encrypted parent + OCFile childFile = new OCFile(FOLDER + "nonEmpty.txt"); + childFile.setStoragePath(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt"); + childFile.setParentId(encryptedParentFolder.getFileId()); + childFile.setEncrypted(false); // explicitly NOT encrypted itself; ancestor is + getStorageManager().saveFile(childFile); + + OCUpload ocUpload = new OCUpload( + FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt", + FOLDER + "nonEmpty.txt", + account.name + ); + + UploadFileOperation newUpload = new UploadFileOperation( + uploadsStorageManager, + connectivityServiceMock, + powerManagementServiceMock, + user, + childFile, + ocUpload, + NameCollisionPolicy.DEFAULT, + FileUploadWorker.LOCAL_BEHAVIOUR_COPY, + targetContext, + false, + false, + getStorageManager() + ); + newUpload.setRemoteFolderToBeCreated(); + newUpload.addRenameUploadListener(() -> { }); + + newUpload.execute(client); + + assertTrue( + "mFile must be marked encrypted, proving encryptedUpload() was called not normalUpload()", + newUpload.getFile().isEncrypted() + ); + + assertNotNull( + "decryptedRemotePath must be set, which only encryptedUpload() does", + newUpload.getDecryptedRemotePath() + ); + assertFalse( + "decryptedRemotePath must be non-empty, which only encryptedUpload() does", + newUpload.getDecryptedRemotePath().isEmpty() + ); + } + private void verifyStoragePath(OCFile file) { assertEquals(FileStorageUtils.getSavePath(account.name) + FOLDER + file.getDecryptedFileName(), file.getStoragePath()); From ec910ddab81477f0fcfb19bf2910f4a416f58eb2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Apr 2026 10:59:14 +0200 Subject: [PATCH 3/5] wip Signed-off-by: alperozturk96 --- .../android/utils/EncryptionUtilsV2IT.kt | 245 +++++------------- .../android/utils/EncryptionUtilsV2.kt | 58 ++--- 2 files changed, 98 insertions(+), 205 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt index 6a0e8f140c87..49408fd9a812 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt @@ -25,7 +25,6 @@ import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFi import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.util.EncryptionTestIT import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue import org.junit.Assert.assertNotEquals import org.junit.Test @@ -405,11 +404,61 @@ class EncryptionUtilsV2IT : EncryptionIT() { arbitraryDataProvider ) - encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 0) + val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encrypted) + + encryptionUtilsV2.verifyMetadata(signature, encrypted, metadataFile, 0) assertTrue(true) } + @Test + fun verifySignedDataWhenGivenValidArgumentsShouldReturnTrue() { + val message = """ + { + "metadata" : { + "authenticationTag" : "NMfZ0KsC4Q8Le/5BYg6iew==", + "ciphertext" : "EtVOZuwijECmRI/7ZGqybveA4yzkNtMJxDy07B+HtNEx75qv/RR897BEEuN+fWVCJ61mOvsg9/d5WdrUliGOzVA0nwa+V+eq08mDCs5ill19+9zCBv832OwfUBqF+UreNSfwlLB83QN0bVl5ItsYhy4HcIUzdHi8RV3ypYBV2lfcNJUibvk0x9nQqwLhDwt7/kFiDqJ7", + "nonce" : "fI5rq9dd2nMjiP/x" + }, + "users" : [ { + "certificate" : "-----BEGIN CERTIFICATE-----\nMIIC7jCCAdagAwIBAgIBADANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDEwVhZG1p\nbjAeFw0yNjA0MjIxMzQwMjJaFw00NjA0MTcxMzQwMjJaMBAxDjAMBgNVBAMTBWFk\nbWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPloRFaKUf+Rb0W9\ngMWTf+3ASW4nUUG6IjAgtaCHSCAmvU7npzFZe84ZYJCU06BiDE/RyRfbNqgPQmsO\neqJJOcVojSKiicw20cwz+JLvtTKhZ/E0Y/E7y1b7pPx2gAdFX46KEUei+QcOA+Sh\nBhN67yAgmkB9JmXUfajpbnF1YlpXyKKUrtv/tV46sJIUSjA1x3K1xDrKKtSwhriK\nWcKAOLF1Do5Jaq0mFNPKXYr6vloic61A9cRjsCpwM5PcRLKh5vzqWKPMY7QgbIBu\nITGp8S2AAPrTSHCMGjSKExnpVnnmg22vh+LOR39JrqKcmBqd1fkGEBKGbGPYXinS\nSF6MLQIDAQABo1MwUTAdBgNVHQ4EFgQUoBFxUd+8eaa4T6wJWDyCrITFx6UwHwYD\nVR0jBBgwFoAUoBFxUd+8eaa4T6wJWDyCrITFx6UwDwYDVR0TAQH/BAUwAwEB/zAN\nBgkqhkiG9w0BAQUFAAOCAQEAa3sN5UjPbPGJ9Ne/qOdGFBoXkynQCLL3/zhhf2U2\nNSCIbmm2KJDNlbsgHTOShpUlWQlFUyXyELRIVUZWahcjgS4aAdnVyc5+j51Rfpns\njgtLSIwRFxCF/XlhL4OZwd0AcP+lVYpwqg/9oHjMsoEhlJf+R2dLg748346C95/8\nNb8YgAXC43eqBiz7JC7R2VyMW1n8Ce8c8III/tZpZJGzlyu1ckVoOvw4ZYZd/O3B\n/d/MfhgD5E5N+PhTPaIWpdyn0+9Q3WbNQtgjq7DW4NGXbwGiF34jJsLEE5rrhKWk\nhVM7gUpNr6YhtWzO64ZZPGHXb5f5HD9j3vYAAJ+8a9VTEg==\n-----END CERTIFICATE-----", + "encryptedMetadataKey" : "FanlaJty6rhcjFfLzERYH6UqeZq+KprKazD2KRqeqeNgonubGD/ckyrLtHSLHczfAGFxk0kL2p77MJ9sbSM+TIlAUrq1JLMOq1dnPldAI7tTb77+fApkiuqmJZhmOEe+dLyewEBegDGQS6M6rlQ+YfCQPnhYb+KAFUm+DzRL3iXCAeO/t+hqml1vb0zjI1F8zsPV5z0PhuqpOZcr2Ao+hpKVpozqNJhI2HSTRjCz/C++N83qE+YqvUYywp4U2LpLRGnGLl2QHyCcCmCFPhFlWuCxTHCe7E8LGcGQ31wRySjKDjO/6YXWEKQy2/k9H0G081YP89mUQzpi53hQ2UhEFw==", + "userId" : "admin" + } ], + "version" : "2.0" +} + """.trimIndent() + + val signature = """ + MIIE1wYJKoZIhvcNAQcCoIIEyDCCBMQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGgggLyMIIC7jCCAdagAwIBAgIBADANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDEwVhZG1pbjAeFw0yNjA0MjIxMzQwMjJaFw00NjA0MTcxMzQwMjJaMBAxDjAMBgNVBAMTBWFkbWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPloRFaKUf+Rb0W9gMWTf+3ASW4nUUG6IjAgtaCHSCAmvU7npzFZe84ZYJCU06BiDE/RyRfbNqgPQmsOeqJJOcVojSKiicw20cwz+JLvtTKhZ/E0Y/E7y1b7pPx2gAdFX46KEUei+QcOA+ShBhN67yAgmkB9JmXUfajpbnF1YlpXyKKUrtv/tV46sJIUSjA1x3K1xDrKKtSwhriKWcKAOLF1Do5Jaq0mFNPKXYr6vloic61A9cRjsCpwM5PcRLKh5vzqWKPMY7QgbIBuITGp8S2AAPrTSHCMGjSKExnpVnnmg22vh+LOR39JrqKcmBqd1fkGEBKGbGPYXinSSF6MLQIDAQABo1MwUTAdBgNVHQ4EFgQUoBFxUd+8eaa4T6wJWDyCrITFx6UwHwYDVR0jBBgwFoAUoBFxUd+8eaa4T6wJWDyCrITFx6UwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAa3sN5UjPbPGJ9Ne/qOdGFBoXkynQCLL3/zhhf2U2NSCIbmm2KJDNlbsgHTOShpUlWQlFUyXyELRIVUZWahcjgS4aAdnVyc5+j51RfpnsjgtLSIwRFxCF/XlhL4OZwd0AcP+lVYpwqg/9oHjMsoEhlJf+R2dLg748346C95/8Nb8YgAXC43eqBiz7JC7R2VyMW1n8Ce8c8III/tZpZJGzlyu1ckVoOvw4ZYZd/O3B/d/MfhgD5E5N+PhTPaIWpdyn0+9Q3WbNQtgjq7DW4NGXbwGiF34jJsLEE5rrhKWkhVM7gUpNr6YhtWzO64ZZPGHXb5f5HD9j3vYAAJ+8a9VTEjGCAakwggGlAgEAMBUwEDEOMAwGA1UEAxMFYWRtaW4CAQAwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMC8GCSqGSIb3DQEJBDEiBCC0buwKgzbVlARGgIiuAZldgZrT4TWMt5IhjuxKwdlBwTAcBgkqhkiG9w0BCQUxDxcNMjYwNDIzMDgyNjI1WjALBgkqhkiG9w0BAQsEggEAetgtsJ1k6BmpbHzWwBsnNc5nEcKz+dHUn3c4+JmoukSCuoYRKmtNY0uJ3XlqqVKkBWD+a6rXBtJPZqH+Y0tc0s/IHY3lztoFB+v2KQwK1KI/8C67qmpwgPAGoMhpzpa1+s4tcsQbNN/hSH25giWT+MwbUBV8Z4yZhdZFrinjwN8teHOaA+jzIpz5s395uFtxznANoo5EX8g1JrN0iPtCWV4e+70+8TLSj0qzX0Kg+pAE4SyImM/6V2GEkR+js+vVvJcJaTAH+/+svBnfT6/W8ElJPr7zLD5tsgHLkyEr1ydgV+rEjE+M/Aoaz9x1iU5wBJ1tbak4fMvGR7DSExFUOA== + """.trimIndent() + + val cert = """ + -----BEGIN CERTIFICATE----- + MIIC7jCCAdagAwIBAgIBADANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDEwVhZG1p + bjAeFw0yNjA0MjIxMzQwMjJaFw00NjA0MTcxMzQwMjJaMBAxDjAMBgNVBAMTBWFk + bWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPloRFaKUf+Rb0W9 + gMWTf+3ASW4nUUG6IjAgtaCHSCAmvU7npzFZe84ZYJCU06BiDE/RyRfbNqgPQmsO + eqJJOcVojSKiicw20cwz+JLvtTKhZ/E0Y/E7y1b7pPx2gAdFX46KEUei+QcOA+Sh + BhN67yAgmkB9JmXUfajpbnF1YlpXyKKUrtv/tV46sJIUSjA1x3K1xDrKKtSwhriK + WcKAOLF1Do5Jaq0mFNPKXYr6vloic61A9cRjsCpwM5PcRLKh5vzqWKPMY7QgbIBu + ITGp8S2AAPrTSHCMGjSKExnpVnnmg22vh+LOR39JrqKcmBqd1fkGEBKGbGPYXinS + SF6MLQIDAQABo1MwUTAdBgNVHQ4EFgQUoBFxUd+8eaa4T6wJWDyCrITFx6UwHwYD + VR0jBBgwFoAUoBFxUd+8eaa4T6wJWDyCrITFx6UwDwYDVR0TAQH/BAUwAwEB/zAN + BgkqhkiG9w0BAQUFAAOCAQEAa3sN5UjPbPGJ9Ne/qOdGFBoXkynQCLL3/zhhf2U2 + NSCIbmm2KJDNlbsgHTOShpUlWQlFUyXyELRIVUZWahcjgS4aAdnVyc5+j51Rfpns + jgtLSIwRFxCF/XlhL4OZwd0AcP+lVYpwqg/9oHjMsoEhlJf+R2dLg748346C95/8 + Nb8YgAXC43eqBiz7JC7R2VyMW1n8Ce8c8III/tZpZJGzlyu1ckVoOvw4ZYZd/O3B + /d/MfhgD5E5N+PhTPaIWpdyn0+9Q3WbNQtgjq7DW4NGXbwGiF34jJsLEE5rrhKWk + hVM7gUpNr6YhtWzO64ZZPGHXb5f5HD9j3vYAAJ+8a9VTEg== + -----END CERTIFICATE----- + """.trimIndent() + + val certs = listOf(EncryptionUtils.convertCertFromString(cert)) + val signedData = encryptionUtilsV2.getSignedData(signature, message) + assertTrue(encryptionUtilsV2.verifySignedData(signedData, certs)) + } + private fun generateDecryptedFileV1(): com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile = com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile().apply { encrypted = Data().apply { @@ -640,173 +689,6 @@ class EncryptionUtilsV2IT : EncryptionIT() { assertEquals("this is a test.\n", gunzip) } - @Test - fun verifyMetadataEmptyFolder() { - val folder = OCFile("/e/") - val enc1 = MockUser("enc1", "Nextcloud") - - val metadataKey = EncryptionUtils.generateKey() - val metadata = DecryptedMetadata( - mutableListOf(), - false, - 0, - mutableMapOf(), - mutableMapOf(), - metadataKey - ) - // checksum must be present before encrypting, otherwise verifyMetadata returns false - metadata.keyChecksums.add(encryptionUtilsV2.hashMetadataKey(metadataKey)) - - val metadataFile = DecryptedFolderMetadataFile( - metadata, - mutableListOf(DecryptedUser(enc1.accountName, enc1Cert, null)), - mutableMapOf() - ) - - val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( - metadataFile, - enc1UserId, - folder, - storageManager, - client, - enc1PrivateKey, - user, - targetContext, - arbitraryDataProvider - ) - - val result = encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 0) - - assertTrue(result) - assertEquals(0, metadataFile.metadata.files.size) - assertEquals(0, metadataFile.metadata.folders.size) - assertEquals(0, metadataFile.metadata.counter) - } - - @Test - fun verifyMetadataWithOneFile() { - val folder = OCFile("/e/") - val enc1 = MockUser("enc1", "Nextcloud") - - val metadata = DecryptedMetadata( - mutableListOf(), - false, - 1, - mutableMapOf(), - mutableMapOf( - Pair( - EncryptionUtils.generateUid(), - DecryptedFile( - "document.pdf", - "application/pdf", - "initializationVector", - "authenticationTag", - "key1" - ) - ) - ), - EncryptionUtils.generateKey() - ) - metadata.keyChecksums.add(encryptionUtilsV2.hashMetadataKey(metadata.metadataKey)) - - val metadataFile = DecryptedFolderMetadataFile( - metadata, - mutableListOf(DecryptedUser(enc1.accountName, enc1Cert, null)), - mutableMapOf() - ) - - val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( - metadataFile, - enc1UserId, - folder, - storageManager, - client, - enc1PrivateKey, - user, - targetContext, - arbitraryDataProvider - ) - - val result = encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 0) - - assertTrue(result) - assertEquals(1, metadataFile.metadata.files.size) - assertEquals("document.pdf", metadataFile.metadata.files.values.first().filename) - } - - @Test - fun verifyMetadataCounterTooOld() { - val folder = OCFile("/e/") - val enc1 = MockUser("enc1", "Nextcloud") - val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) - - val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( - metadataFile, - enc1UserId, - folder, - storageManager, - client, - enc1PrivateKey, - user, - targetContext, - arbitraryDataProvider - ) - - // counter in metadata is 1, passing oldCounter=2 should fail - val result = encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 2) - - assertFalse(result) - } - - @Test - fun verifyMetadataInvalidSignature() { - val folder = OCFile("/e/") - val enc1 = MockUser("enc1", "Nextcloud") - val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) - - val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( - metadataFile, - enc1UserId, - folder, - storageManager, - client, - enc1PrivateKey, - user, - targetContext, - arbitraryDataProvider - ) - - val result = encryptionUtilsV2.verifyMetadata(enc2PrivateKey, encrypted, metadataFile, 0) - - assertFalse(result) - } - - @Test - fun verifyMetadataChecksumMismatch() { - val folder = OCFile("/e/") - val enc1 = MockUser("enc1", "Nextcloud") - val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) - - val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( - metadataFile, - enc1UserId, - folder, - storageManager, - client, - enc1PrivateKey, - user, - targetContext, - arbitraryDataProvider - ) - - // Replace the metadata key with a new one whose hash is NOT in keyChecksums - metadataFile.metadata.metadataKey = EncryptionUtils.generateKey() - - val result = encryptionUtilsV2.verifyMetadata(enc1PrivateKey, encrypted, metadataFile, 0) - - assertFalse(result) - } - @Throws(Throwable::class) @Test fun testSigning() { @@ -839,15 +721,22 @@ class EncryptionUtilsV2IT : EncryptionIT() { |Rei/RGBQ==","userId": "john"}],"version": "2"} """.trimMargin() + val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) val certificateT1 = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey) val certificateEnc2 = EncryptionUtils.convertCertFromString(enc2Cert) + val signed = encryptionUtilsV2.signMessage( + certificateT1, + privateKey, + metadata + ) + val certs = listOf( certificateEnc2, certificateT1 ) - assertTrue(encryptionUtilsV2.verifySignedData(metadata, encryptionTestUtils.t1PrivateKey, certs)) + assertTrue(encryptionUtilsV2.verifySignedData(signed, certs)) } @Throws(Throwable::class) @@ -855,14 +744,21 @@ class EncryptionUtilsV2IT : EncryptionIT() { fun sign() { val sut = "randomstring123" + val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) val certificate = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey) + val signed = encryptionUtilsV2.signMessage( + certificate, + privateKey, + sut + ) + val certs = listOf( EncryptionUtils.convertCertFromString(enc2Cert), certificate ) - assertTrue(encryptionUtilsV2.verifySignedData(sut, encryptionTestUtils.t1PrivateKey, certs)) + assertTrue(encryptionUtilsV2.verifySignedData(signed, certs)) } @Test @@ -939,9 +835,6 @@ class EncryptionUtilsV2IT : EncryptionIT() { ) // V1 doesn't have decryptedMetadataKey so that we can ignore it for comparison - for (user in decryptedFolderMetadata1.users) { - user.decryptedMetadataKey = null - } for (user in decryptedFolderMetadata2.users) { user.decryptedMetadataKey = null } @@ -949,8 +842,8 @@ class EncryptionUtilsV2IT : EncryptionIT() { // compare assertTrue( EncryptionTestIT.compareJsonStrings( - EncryptionUtils.serializeJSON(decryptedFolderMetadata1, true), - EncryptionUtils.serializeJSON(decryptedFolderMetadata2, true) + EncryptionUtils.serializeJSON(decryptedFolderMetadata1), + EncryptionUtils.serializeJSON(decryptedFolderMetadata2) ) ) } diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index 9cddda91ae03..b9a81dc8d7d7 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -41,6 +41,8 @@ import com.owncloud.android.lib.resources.e2ee.StoreMetadataV2RemoteOperation import com.owncloud.android.lib.resources.e2ee.UpdateMetadataV2RemoteOperation import com.owncloud.android.operations.UploadException import org.apache.commons.httpclient.HttpStatus +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.cms.ContentInfo import org.bouncycastle.cert.jcajce.JcaCertStore import org.bouncycastle.cms.CMSProcessableByteArray import org.bouncycastle.cms.CMSSignedData @@ -229,7 +231,7 @@ class EncryptionUtilsV2 { ) } - if (!verifyMetadata(privateKey, metadataFile, decryptedFolderMetadataFile, oldCounter)) { + if (!verifyMetadata(signature, metadataFile, decryptedFolderMetadataFile, oldCounter)) { throw IllegalStateException("Metadata is corrupt!") } @@ -850,7 +852,7 @@ class EncryptionUtilsV2 { @Suppress("ReturnCount") fun verifyMetadata( - privateKey: String, + signature: String, encryptedFolderMetadataFile: EncryptedFolderMetadataFile, decryptedFolderMetadataFile: DecryptedFolderMetadataFile, oldCounter: Long @@ -860,10 +862,11 @@ class EncryptionUtilsV2 { return false } - val folderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadataFile, true) + val message = EncryptionUtils.serializeJSON(encryptedFolderMetadataFile, true) val certs = decryptedFolderMetadataFile.users.map { EncryptionUtils.convertCertFromString(it.certificate) } + val signedData = getSignedData(signature, message) - if (certs.isNotEmpty() && !verifySignedData(folderMetadata, privateKey, certs)) { + if (certs.isNotEmpty() && !verifySignedData(signedData, certs)) { MainApp.showMessage(R.string.e2e_signature_does_not_match) return false } @@ -876,34 +879,31 @@ class EncryptionUtilsV2 { return true } - @Suppress("TooGenericExceptionCaught") - fun verifySignedData(folderMetadata: String, privateKey: String, certs: List): Boolean { - for (cert in certs) { - try { - val privateKey = EncryptionUtils.PEMtoPrivateKey(privateKey) - val data = signMessage( - cert, - privateKey, - folderMetadata - ) - val signers = data.signerInfos.signers - if (signers.isEmpty()) { - Log_OC.e(TAG, "verifySignedData: no signers found in CMSSignedData") - continue - } + fun getSignedData(base64encodedSignature: String, message: String): CMSSignedData { + val signature = EncryptionUtils.decodeStringToBase64Bytes(base64encodedSignature) + val asn1Signature = ASN1Sequence.fromByteArray(signature) + val contentInfo = ContentInfo.getInstance(asn1Signature) - val signer = signers.first() as SignerInformation - val verifierBuilder = JcaSimpleSignerInfoVerifierBuilder() - if (signer.verify(verifierBuilder.build(cert.publicKey))) { - return true - } - } catch (e: Exception) { - Log_OC.e(TAG, "exception verifySignedData: $e") - continue + val encodedMessage = EncryptionUtils.encodeStringToBase64String(message) + val messageData = encodedMessage.toByteArray() + val cmsProcessableByteArray = CMSProcessableByteArray(messageData) + + return CMSSignedData(cmsProcessableByteArray, contentInfo) + } + + @Suppress("TooGenericExceptionCaught") + fun verifySignedData(data: CMSSignedData, certs: List): Boolean { + val signer = data.signerInfos.signers.first() as SignerInformation + val verifierBuilder = JcaSimpleSignerInfoVerifierBuilder() + + return certs.any { cert -> + runCatching { + signer.verify(verifierBuilder.build(cert.publicKey)) + }.getOrElse { + Log_OC.e(TAG, "Exception verifySignedData: $it") + false } } - - return false } private fun signMessage(cert: X509Certificate, key: PrivateKey, data: ByteArray): CMSSignedData { From 153f266a9f9bd8ccff7c0c12a9c15f5b2fb4d978 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Apr 2026 08:49:18 +0200 Subject: [PATCH 4/5] wip Signed-off-by: alperozturk96 --- .../android/utils/EncryptionUtils.java | 1 + .../android/utils/EncryptionUtilsV2.kt | 75 +++++++++++++++---- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java index 078bc598b357..fb686c8a894b 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java @@ -1274,6 +1274,7 @@ public static Pair retrieveMetadata(OCFile client, parentFile.getE2eCounter(), getMetadataOperationResult.getResultData().getSignature(), + getMetadataOperationResult.getResultData().getMetadata(), user, context, arbitraryDataProvider) diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index b9a81dc8d7d7..d3a59d872f5b 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -150,6 +150,7 @@ class EncryptionUtilsV2 { client: OwnCloudClient, oldCounter: Long, signature: String, + rawMetadata: String, user: User, context: Context, arbitraryDataProvider: ArbitraryDataProvider @@ -231,7 +232,7 @@ class EncryptionUtilsV2 { ) } - if (!verifyMetadata(signature, metadataFile, decryptedFolderMetadataFile, oldCounter)) { + if (!verifyMetadata(signature, rawMetadata, decryptedFolderMetadataFile, oldCounter)) { throw IllegalStateException("Metadata is corrupt!") } @@ -335,6 +336,7 @@ class EncryptionUtilsV2 { client, topMost.e2eCounter, result.resultData.signature, + result.resultData.metadata, user, context, arbitraryDataProvider @@ -606,6 +608,7 @@ class EncryptionUtilsV2 { client, folder.e2eCounter, metadataResponse.signature, + metadataResponse.metadata, user, context, arbitraryDataProvider @@ -853,7 +856,7 @@ class EncryptionUtilsV2 { @Suppress("ReturnCount") fun verifyMetadata( signature: String, - encryptedFolderMetadataFile: EncryptedFolderMetadataFile, + rawMetadata: String, decryptedFolderMetadataFile: DecryptedFolderMetadataFile, oldCounter: Long ): Boolean { @@ -862,9 +865,14 @@ class EncryptionUtilsV2 { return false } - val message = EncryptionUtils.serializeJSON(encryptedFolderMetadataFile, true) - val certs = decryptedFolderMetadataFile.users.map { EncryptionUtils.convertCertFromString(it.certificate) } - val signedData = getSignedData(signature, message) + val prepared = prepareRawMetadata(rawMetadata) + val base64Json = EncryptionUtils.encodeStringToBase64String(prepared) + val messageData = base64Json.toByteArray(Charsets.ISO_8859_1) + + val certs = decryptedFolderMetadataFile.users + .map { EncryptionUtils.convertCertFromString(it.certificate) } + + val signedData = getSignedData(signature, messageData) if (certs.isNotEmpty() && !verifySignedData(signedData, certs)) { MainApp.showMessage(R.string.e2e_signature_does_not_match) @@ -876,26 +884,67 @@ class EncryptionUtilsV2 { MainApp.showMessage(R.string.e2e_hash_not_found) return false } + return true } - fun getSignedData(base64encodedSignature: String, message: String): CMSSignedData { + @VisibleForTesting + fun prepareRawMetadata(rawMetadata: String): String { + val parsed = com.google.gson.JsonParser.parseString(rawMetadata) + val sorted = sortJsonElement(parsed) + return com.google.gson.GsonBuilder() + .disableHtmlEscaping() + .serializeNulls() + .create() + .toJson(sorted) + } + + private fun sortJsonElement(element: com.google.gson.JsonElement): com.google.gson.JsonElement = when { + element.isJsonObject -> { + val sortedObj = com.google.gson.JsonObject() + element.asJsonObject.entrySet() + .filter { it.key != "filedrop" } + .sortedBy { it.key } + .forEach { (key, value) -> sortedObj.add(key, sortJsonElement(value)) } + sortedObj + } + element.isJsonArray -> { + val sortedArr = com.google.gson.JsonArray() + element.asJsonArray.forEach { sortedArr.add(sortJsonElement(it)) } + sortedArr + } + else -> element + } + + fun getSignedData(base64encodedSignature: String, messageData: ByteArray): CMSSignedData { val signature = EncryptionUtils.decodeStringToBase64Bytes(base64encodedSignature) val asn1Signature = ASN1Sequence.fromByteArray(signature) val contentInfo = ContentInfo.getInstance(asn1Signature) - - val encodedMessage = EncryptionUtils.encodeStringToBase64String(message) - val messageData = encodedMessage.toByteArray() - val cmsProcessableByteArray = CMSProcessableByteArray(messageData) - - return CMSSignedData(cmsProcessableByteArray, contentInfo) + return CMSSignedData(CMSProcessableByteArray(messageData), contentInfo) } @Suppress("TooGenericExceptionCaught") fun verifySignedData(data: CMSSignedData, certs: List): Boolean { val signer = data.signerInfos.signers.first() as SignerInformation - val verifierBuilder = JcaSimpleSignerInfoVerifierBuilder() + val signedAttrs = signer.signedAttributes + if (signedAttrs != null) { + val messageDigestAttr = signedAttrs.get(org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers.pkcs_9_at_messageDigest) + if (messageDigestAttr != null) { + val digestValue = (messageDigestAttr.attrValues.getObjectAt(0) as org.bouncycastle.asn1.DEROctetString).octets + Log_OC.d(TAG, "Expected message digest (from signature): ${EncryptionUtils.encodeBytesToBase64String(digestValue)}") + } + } + + val contentDigest = java.security.MessageDigest.getInstance("SHA-256") + .digest((data.signedContent as CMSProcessableByteArray).let { + val out = ByteArrayOutputStream() + it.write(out) + out.toByteArray() + }) + Log_OC.d(TAG, "Actual content digest: ${EncryptionUtils.encodeBytesToBase64String(contentDigest)}") + + val verifierBuilder = JcaSimpleSignerInfoVerifierBuilder() return certs.any { cert -> runCatching { signer.verify(verifierBuilder.build(cert.publicKey)) From 81c341f169c4054e27eb7646c311bd041f24d74e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Apr 2026 09:29:01 +0200 Subject: [PATCH 5/5] wip Signed-off-by: alperozturk96 --- .../android/utils/EncryptionUtilsV2.kt | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index d3a59d872f5b..42c40e51cddc 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -866,15 +866,48 @@ class EncryptionUtilsV2 { } val prepared = prepareRawMetadata(rawMetadata) - val base64Json = EncryptionUtils.encodeStringToBase64String(prepared) - val messageData = base64Json.toByteArray(Charsets.ISO_8859_1) + val messageData = EncryptionUtils.encodeStringToBase64String(prepared).toByteArray(Charsets.ISO_8859_1) + val users = decryptedFolderMetadataFile.users + val signedData = getSignedData(signature, messageData) + + val signerInfo = signedData.signerInfos.signers.first() as SignerInformation + val issuer = org.bouncycastle.asn1.x500.X500Name.getInstance( + (signerInfo.sid as org.bouncycastle.cms.SignerId).issuer + ) + val rdns = issuer.getRDNs(org.bouncycastle.asn1.x500.style.BCStyle.CN) + val signerUserId = rdns.firstOrNull()?.first?.value?.toString() - val certs = decryptedFolderMetadataFile.users - .map { EncryptionUtils.convertCertFromString(it.certificate) } - val signedData = getSignedData(signature, messageData) + Log_OC.d(TAG, "Signer userId from CMS: $signerUserId") + + val signerUser = users.find { it.userId == signerUserId } + if (signerUser == null) { + Log_OC.e(TAG, "Signer not found in users array: $signerUserId") + MainApp.showMessage(R.string.e2e_signature_does_not_match) + return false + } + + val signerCert = EncryptionUtils.convertCertFromString(signerUser.certificate) + + val bcProvider = org.bouncycastle.jce.provider.BouncyCastleProvider() + java.security.Security.removeProvider("BC") + java.security.Security.insertProviderAt(bcProvider, 1) + + val verifier = JcaSimpleSignerInfoVerifierBuilder() + .setProvider(bcProvider) + .build(signerCert) + + val directResult = runCatching { + signerInfo.verify(verifier) + }.getOrElse { + Log_OC.e(TAG, "signerInfo.verify() exception: ${it.javaClass.name}: ${it.message}") + it.cause?.let { c -> Log_OC.e(TAG, " caused by: ${c.javaClass.name}: ${c.message}") } + false + } + Log_OC.d(TAG, "signerInfo.verify() result: $directResult") - if (certs.isNotEmpty() && !verifySignedData(signedData, certs)) { + if (!directResult) { + Log_OC.e(TAG, "Signature verification failed for user: $signerUserId") MainApp.showMessage(R.string.e2e_signature_does_not_match) return false }