diff --git a/tests/cli_tests/zboxcli_share_file_test.go b/tests/cli_tests/zboxcli_share_file_test.go index 106d5f4b3c..5870a45a79 100644 --- a/tests/cli_tests/zboxcli_share_file_test.go +++ b/tests/cli_tests/zboxcli_share_file_test.go @@ -1553,6 +1553,1211 @@ func TestShareFile(testSetup *testing.T) { require.Equal(t, "Error: remotepath flag is missing", output[0], "share file - Unexpected output", strings.Join(output, "\n")) }) + + // ---------------------------------------------------------------------- + // Encrypted folder share regression suite — fix/pre-reuse-folder-entropy + // + // The gosdk fix (commit 9dfc92c4) made folder-level shares use + // signingPrivateKey-derived entropy when the ref is a directory, mirroring + // chunked_upload on SignatureV2 allocations. Pre-fix, NewDirectoryRef() + // never set EncryptionVersion, so folder shares fell through to mnemonic + // entropy, producing "Invalid Ciphertext in reEncrypt, C4 != H5" on every + // download. + // + // Each subtest below materializes the shared directory with `zbox createdir` + // — the most direct caller of NewDirectoryRef() — to exercise the patched + // code path explicitly. + // ---------------------------------------------------------------------- + + t.RunWithTimeout("Encrypted folder share - empty folder share download added top-level file - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 1) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + require.NotEqual(t, "", authTicket) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + remotePath := "/shared/" + filepath.Base(file) + output, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": file, + "remotepath": remotePath, + "encrypt": "", + }, true) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + require.Contains(t, output[1], StatusCompletedCB) + + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": remotePath, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - pre-existing top-level file - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + remotePath := "/shared/" + filepath.Base(file) + output, err := uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": file, + "remotepath": remotePath, + "encrypt": "", + }, true) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + require.Contains(t, output[1], StatusCompletedCB) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err = shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 1) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": remotePath, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - pre-existing nested file - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/shared/nested", true) + require.Nil(t, err) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + remotePath := "/shared/nested/" + filepath.Base(file) + output, err := uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": file, + "remotepath": remotePath, + "encrypt": "", + }, true) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err = shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": remotePath, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - add top-level file after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + fileA := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileA, 256)) + remoteA := "/shared/" + filepath.Base(fileA) + output, err := uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": fileA, + "remotepath": remoteA, + "encrypt": "", + }, true) + require.Nil(t, err, strings.Join(output, "\n")) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err = shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + os.Remove(fileA) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileA, + "authticket": authTicket, + "remotepath": remoteA, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + + // Owner adds a NEW file to the already-shared folder + fileB := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileB, 256)) + remoteB := "/shared/" + filepath.Base(fileB) + output, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": fileB, + "remotepath": remoteB, + "encrypt": "", + }, true) + require.Nil(t, err, strings.Join(output, "\n")) + + // Recipient downloads new file with the SAME ticket + os.Remove(fileB) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileB, + "authticket": authTicket, + "remotepath": remoteB, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - add nested folder after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + fileA := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileA, 256)) + remoteA := "/shared/" + filepath.Base(fileA) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": fileA, + "remotepath": remoteA, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Owner adds a NEW nested subfolder + file AFTER share + _, err = createDir(t, configPath, allocationID, "/shared/nested", true) + require.Nil(t, err) + + fileN := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileN, 256)) + remoteN := "/shared/nested/" + filepath.Base(fileN) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": fileN, + "remotepath": remoteN, + "encrypt": "", + }, true) + require.Nil(t, err) + + // Recipient downloads the file in the newly-added subfolder + os.Remove(fileN) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileN, + "authticket": authTicket, + "remotepath": remoteN, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - add deeply nested folders after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Owner builds a deep tree AFTER share — exercises NewDirectoryRef at depth + for _, d := range []string{"/shared/a", "/shared/a/b", "/shared/a/b/c"} { + _, err = createDir(t, configPath, allocationID, d, true) + require.Nil(t, err, "failed to create dir %s", d) + } + + fileD := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileD, 256)) + remoteD := "/shared/a/b/c/" + filepath.Base(fileD) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": fileD, + "remotepath": remoteD, + "encrypt": "", + }, true) + require.Nil(t, err) + + os.Remove(fileD) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileD, + "authticket": authTicket, + "remotepath": remoteD, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Len(t, output, 2) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - update top-level file after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + // v1 source + v1Src := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(v1Src, 256)) + v1Bytes, err := os.ReadFile(v1Src) + require.Nil(t, err) + + remotePath := "/shared/" + filepath.Base(v1Src) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": v1Src, + "remotepath": remotePath, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Recipient downloads v1 to a fresh path + v1Dst := generateRandomTestFileName(t) + os.Remove(v1Dst) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": v1Dst, + "authticket": authTicket, + "remotepath": remotePath, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + gotV1, err := os.ReadFile(v1Dst) + require.Nil(t, err) + require.Equal(t, v1Bytes, gotV1, "recipient should get v1 content before update") + + // Owner overwrites with v2 — distinct fixed bytes so the compare is meaningful + v2Src := generateRandomTestFileName(t) + v2Bytes := []byte(strings.Repeat("V2-DISTINCT-CONTENT-PAYLOAD-FOR-ENCRYPTED-FOLDER-SHARE-TEST.", 5)) + require.Nil(t, os.WriteFile(v2Src, v2Bytes, 0o644)) + _, err = updateFileWithWallet(t, walletOwner, configPath, map[string]interface{}{ + "allocation": allocationID, + "remotepath": remotePath, + "localpath": v2Src, + }, true) + require.Nil(t, err) + + // Recipient downloads v2 with SAME ticket to a fresh path + v2Dst := generateRandomTestFileName(t) + os.Remove(v2Dst) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": v2Dst, + "authticket": authTicket, + "remotepath": remotePath, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + gotV2, err := os.ReadFile(v2Dst) + require.Nil(t, err) + require.Equal(t, v2Bytes, gotV2, "recipient should get v2 content after update with same ticket") + require.NotEqual(t, v1Bytes, gotV2, "v2 must differ from v1 — not a tautological compare") + }) + + t.RunWithTimeout("Encrypted folder share - update nested file after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/shared/nested", true) + require.Nil(t, err) + + v1Src := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(v1Src, 256)) + v1Bytes, err := os.ReadFile(v1Src) + require.Nil(t, err) + + remotePath := "/shared/nested/" + filepath.Base(v1Src) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": v1Src, + "remotepath": remotePath, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + v2Src := generateRandomTestFileName(t) + v2Bytes := []byte(strings.Repeat("NESTED-V2-PAYLOAD-FOR-ENCRYPTED-FOLDER-SHARE-UPDATE-TEST.", 5)) + require.Nil(t, os.WriteFile(v2Src, v2Bytes, 0o644)) + _, err = updateFileWithWallet(t, walletOwner, configPath, map[string]interface{}{ + "allocation": allocationID, + "remotepath": remotePath, + "localpath": v2Src, + }, true) + require.Nil(t, err) + + v2Dst := generateRandomTestFileName(t) + os.Remove(v2Dst) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": v2Dst, + "authticket": authTicket, + "remotepath": remotePath, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + gotV2, err := os.ReadFile(v2Dst) + require.Nil(t, err) + require.Equal(t, v2Bytes, gotV2, "recipient should get v2 content for nested file") + require.NotEqual(t, v1Bytes, gotV2) + }) + + t.RunWithTimeout("Encrypted folder share - rename top-level file after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + oldName := filepath.Base(file) + oldRemote := "/shared/" + oldName + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": file, + "remotepath": oldRemote, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Rename the file in-place + newName := "renamed_" + oldName + newRemote := "/shared/" + newName + _, err = renameFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "remotepath": oldRemote, + "destname": newName, + }, true) + require.Nil(t, err) + + // Recipient downloads via NEW path with same ticket + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": newRemote, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + + // Old path should fail + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": oldRemote, + }), false) + require.NotNil(t, err, strings.Join(output, "\n")) + require.Contains(t, strings.Join(output, " "), "ref not found") + }) + + t.RunWithTimeout("Encrypted folder share - rename nested file after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/shared/nested", true) + require.Nil(t, err) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + oldName := filepath.Base(file) + oldRemote := "/shared/nested/" + oldName + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": file, + "remotepath": oldRemote, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + newName := "renamed_" + oldName + newRemote := "/shared/nested/" + newName + _, err = renameFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "remotepath": oldRemote, + "destname": newName, + }, true) + require.Nil(t, err) + + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": newRemote, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - rename nested folder after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/shared/nested", true) + require.Nil(t, err) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + baseName := filepath.Base(file) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": file, + "remotepath": "/shared/nested/" + baseName, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Rename the SUBFOLDER itself — strongest folder-entropy regression case + _, err = renameFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "remotepath": "/shared/nested", + "destname": "nested_renamed", + }, true) + require.Nil(t, err) + + // File is now under /shared/nested_renamed/ — same ticket should work + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": "/shared/nested_renamed/" + baseName, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - move file into shared folder after share - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/outside", true) + require.Nil(t, err) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + baseName := filepath.Base(file) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": file, + "remotepath": "/outside/" + baseName, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Move file from outside the share INTO the shared folder + _, err = moveFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "remotepath": "/outside/" + baseName, + "destpath": "/shared", + }, true) + require.Nil(t, err) + + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": "/shared/" + baseName, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - move file within shared folder - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/shared/nested", true) + require.Nil(t, err) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + baseName := filepath.Base(file) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": file, + "remotepath": "/shared/nested/" + baseName, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Move file from nested up to the shared folder + _, err = moveFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "remotepath": "/shared/nested/" + baseName, + "destpath": "/shared", + }, true) + require.Nil(t, err) + + os.Remove(file) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": "/shared/" + baseName, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - delete top-level file - sibling still works - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + fileA := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileA, 256)) + baseA := filepath.Base(fileA) + remoteA := "/shared/" + baseA + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": fileA, + "remotepath": remoteA, + "encrypt": "", + }, true) + require.Nil(t, err) + + fileB := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileB, 256)) + baseB := filepath.Base(fileB) + remoteB := "/shared/" + baseB + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "localpath": fileB, + "remotepath": remoteB, + "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Owner deletes fileA + _, err = deleteFile(t, walletOwner, createParams(map[string]interface{}{ + "allocation": allocationID, + "remotepath": remoteA, + }), true) + require.Nil(t, err) + + // Recipient download of deleted file fails + os.Remove(fileA) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileA, + "authticket": authTicket, + "remotepath": remoteA, + }), false) + require.NotNil(t, err, strings.Join(output, "\n")) + require.Contains(t, strings.Join(output, " "), "ref not found") + + // Sibling fileB still works with same ticket + os.Remove(fileB) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileB, + "authticket": authTicket, + "remotepath": remoteB, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - delete nested file - parent and siblings still work - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/shared/nested", true) + require.Nil(t, err) + + // /shared/a.txt + /shared/nested/n1.txt + /shared/nested/n2.txt + fileA := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileA, 256)) + remoteA := "/shared/" + filepath.Base(fileA) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileA, "remotepath": remoteA, "encrypt": "", + }, true) + require.Nil(t, err) + + fileN1 := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileN1, 256)) + remoteN1 := "/shared/nested/" + filepath.Base(fileN1) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileN1, "remotepath": remoteN1, "encrypt": "", + }, true) + require.Nil(t, err) + + fileN2 := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileN2, 256)) + remoteN2 := "/shared/nested/" + filepath.Base(fileN2) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileN2, "remotepath": remoteN2, "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + _, err = deleteFile(t, walletOwner, createParams(map[string]interface{}{ + "allocation": allocationID, + "remotepath": remoteN1, + }), true) + require.Nil(t, err) + + // Deleted nested file: fail + os.Remove(fileN1) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileN1, + "authticket": authTicket, + "remotepath": remoteN1, + }), false) + require.NotNil(t, err, strings.Join(output, "\n")) + require.Contains(t, strings.Join(output, " "), "ref not found") + + // Sibling nested file still works + os.Remove(fileN2) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileN2, + "authticket": authTicket, + "remotepath": remoteN2, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + + // Top-level still works + os.Remove(fileA) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileA, + "authticket": authTicket, + "remotepath": remoteA, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - delete nested folder - top-level still works - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/shared/nested", true) + require.Nil(t, err) + + fileA := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileA, 256)) + remoteA := "/shared/" + filepath.Base(fileA) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileA, "remotepath": remoteA, "encrypt": "", + }, true) + require.Nil(t, err) + + fileN := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileN, 256)) + remoteN := "/shared/nested/" + filepath.Base(fileN) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileN, "remotepath": remoteN, "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Delete the nested file then the (now empty) folder + _, err = deleteFile(t, walletOwner, createParams(map[string]interface{}{ + "allocation": allocationID, + "remotepath": remoteN, + }), true) + require.Nil(t, err) + // Best-effort delete the empty folder ref; failure here is non-fatal + // because the lifecycle assertion is about content access, not ref cleanup. + _, _ = deleteFile(t, walletOwner, createParams(map[string]interface{}{ + "allocation": allocationID, + "remotepath": "/shared/nested", + }), false) + + // Nested file no longer downloadable + os.Remove(fileN) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileN, + "authticket": authTicket, + "remotepath": remoteN, + }), false) + require.NotNil(t, err, strings.Join(output, "\n")) + require.Contains(t, strings.Join(output, " "), "ref not found") + + // Top-level still works with same ticket + os.Remove(fileA) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileA, + "authticket": authTicket, + "remotepath": remoteA, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - unrelated wallet cannot use recipient ticket - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + + file := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(file, 256)) + remotePath := "/shared/" + filepath.Base(file) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": file, "remotepath": remotePath, "encrypt": "", + }, true) + require.Nil(t, err) + + // userA — legitimate recipient + userA := escapedTestName(t) + "_second" + createWalletForName(userA) + walletA, err := getWalletForName(t, configPath, userA) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletA.ClientID, + "encryptionpublickey": walletA.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // userB — unrelated third wallet attempting to reuse userA's ticket + userB := escapedTestName(t) + "_third" + createWalletForName(userB) + + os.Remove(file) + output, err = downloadFileForWallet(t, userB, configPath, createParams(map[string]interface{}{ + "localpath": file, + "authticket": authTicket, + "remotepath": remotePath, + }), false) + require.NotNil(t, err, strings.Join(output, "\n")) + require.Contains(t, strings.Join(output, " "), "ref not found", + "unrelated wallet must not be able to use a ticket bound to another recipient") + }) + + t.RunWithTimeout("Encrypted folder share - recipient cannot escape via remotepath - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/private", true) + require.Nil(t, err) + + // /shared/a.txt — recipient is allowed to see this + fileA := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileA, 256)) + remoteA := "/shared/" + filepath.Base(fileA) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileA, "remotepath": remoteA, "encrypt": "", + }, true) + require.Nil(t, err) + + // /private/secret.txt — recipient must NOT be able to reach this + fileSecret := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileSecret, 256)) + remoteSecret := "/private/" + filepath.Base(fileSecret) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileSecret, "remotepath": remoteSecret, "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Attempt to download /private/secret.txt with the /shared ticket + os.Remove(fileSecret) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileSecret, + "authticket": authTicket, + "remotepath": remoteSecret, + }), false) + require.NotNil(t, err, strings.Join(output, "\n")) + require.Contains(t, strings.Join(output, " "), "ref not found", + "recipient must not reach files outside the shared folder via remotepath") + + // Control: legit access still works + os.Remove(fileA) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileA, + "authticket": authTicket, + "remotepath": remoteA, + }), false) + require.Nil(t, err, strings.Join(output, "\n")) + require.Contains(t, output[1], StatusCompletedCB) + }) + + t.RunWithTimeout("Encrypted folder share - recipient cannot escape via lookuphash - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/private", true) + require.Nil(t, err) + + fileA := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileA, 256)) + remoteA := "/shared/" + filepath.Base(fileA) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileA, "remotepath": remoteA, "encrypt": "", + }, true) + require.Nil(t, err) + + fileSecret := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileSecret, 256)) + remoteSecret := "/private/" + filepath.Base(fileSecret) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileSecret, "remotepath": remoteSecret, "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Compute the lookup hash of the target file outside the share scope. + // Formula: sha3-256 hex of ":". Source: + // gosdk/zboxcore/fileref/fileref.go:115 (GetReferenceLookup). + secretHashBytes := sha3.Sum256([]byte(allocationID + ":" + remoteSecret)) + secretLookupHash := hex.EncodeToString(secretHashBytes[:]) + + // Attack: pass a manipulated lookuphash for /private/secret.txt + os.Remove(fileSecret) + output, err = downloadFileForWallet(t, receiverWallet, configPath, createParams(map[string]interface{}{ + "localpath": fileSecret, + "authticket": authTicket, + "lookuphash": secretLookupHash, + }), false) + require.NotNil(t, err, strings.Join(output, "\n")) + }) + + t.RunWithTimeout("Encrypted folder share - recipient cannot list sibling folder via authticket - proxy re-encryption", 5*time.Minute, func(t *test.SystemTest) { + walletOwner := escapedTestName(t) + allocationID, _ := createWalletAndAllocation(t, configPath, walletOwner) + + _, err := createDir(t, configPath, allocationID, "/shared", true) + require.Nil(t, err) + _, err = createDir(t, configPath, allocationID, "/private", true) + require.Nil(t, err) + + fileSecret := generateRandomTestFileName(t) + require.Nil(t, createFileWithSize(fileSecret, 256)) + remoteSecret := "/private/" + filepath.Base(fileSecret) + _, err = uploadFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, "localpath": fileSecret, "remotepath": remoteSecret, "encrypt": "", + }, true) + require.Nil(t, err) + + receiverWallet := escapedTestName(t) + "_second" + createWalletForName(receiverWallet) + walletReceiver, err := getWalletForName(t, configPath, receiverWallet) + require.Nil(t, err) + + output, err := shareFile(t, configPath, map[string]interface{}{ + "allocation": allocationID, + "clientid": walletReceiver.ClientID, + "encryptionpublickey": walletReceiver.EncryptionPublicKey, + "remotepath": "/shared", + }) + require.Nil(t, err, strings.Join(output, "\n")) + authTicket, err := extractAuthToken(output[0]) + require.Nil(t, err) + + // Receiver attempts to list /private using the /shared ticket. Inline the + // command so it runs as the receiver wallet — the listAllFilesFromBlobber + // helper hardcodes the test name, which would run as the owner. + listCmd := fmt.Sprintf( + "./zbox list --authticket %s --remotepath /private --json --silent --wallet %s_wallet.json --configDir ./config --config %s", + authTicket, receiverWallet, configPath, + ) + output, err = cliutils.RunCommandWithoutRetry(listCmd) + // Two acceptable outcomes: blobber rejects (err != nil with consensus + // failure) OR returns an empty list (no children). Either proves the + // recipient cannot enumerate sibling folders. + joined := strings.Join(output, " ") + listFailed := err != nil && (strings.Contains(joined, "ref not found") || + strings.Contains(joined, "invalid_path") || + strings.Contains(joined, "auth_ticket") || + strings.Contains(joined, "allocation flag is missing")) + emptyList := err == nil && + (joined == "" || joined == "null" || strings.Contains(joined, "[]")) + require.True(t, listFailed || emptyList, + "recipient must not be able to list sibling folder /private; got: %s", joined) + }) } func shareFile(t *test.SystemTest, cliConfigFilename string, param map[string]interface{}) ([]string, error) {