diff --git a/backend/src/db/migrations/20240930134623_secret-sharing-string-id.ts b/backend/src/db/migrations/20240930134623_secret-sharing-string-id.ts new file mode 100644 index 000000000..6cd197915 --- /dev/null +++ b/backend/src/db/migrations/20240930134623_secret-sharing-string-id.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSharing)) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + // Add a new column + t.string("new_id", 36).nullable(); + }); + + // Copy data from old column to new column + await knex(TableName.SecretSharing).update({ + // @ts-ignore + new_id: knex.raw("id::text") + }); + + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + // Make the new column not nullable + t.string("new_id", 36).notNullable().alter(); + + // Drop the old primary key + t.dropPrimary(); + + // Drop the old id column + t.dropColumn("id"); + + // Rename the new column to 'id' + t.renameColumn("new_id", "id"); + + // Set the new column as primary key + t.primary(["id"]); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSharing)) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + // Add a new UUID column + t.uuid("new_id").nullable(); + }); + + // Copy data from string id to UUID, ensuring valid UUID format + await knex(TableName.SecretSharing).update({ + // @ts-ignore + new_id: knex.raw("id::uuid") + }); + + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + // Make the new column not nullable + t.uuid("new_id").notNullable().alter(); + + // Drop the old primary key + t.dropPrimary(); + + // Drop the old id column + t.dropColumn("id"); + + // Rename the new column to 'id' + t.renameColumn("new_id", "id"); + + // Set the new column as primary key + t.primary(["id"]); + }); + } +} diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index d7597af6e..7490269d7 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -10,7 +10,6 @@ import { zodBuffer } from "@app/lib/zod"; import { TImmutableDBKeys } from "./models"; export const SecretSharingSchema = z.object({ - id: z.string().uuid(), encryptedValue: z.string().nullable().optional(), iv: z.string().nullable().optional(), tag: z.string().nullable().optional(), @@ -25,7 +24,8 @@ export const SecretSharingSchema = z.object({ name: z.string().nullable().optional(), lastViewedAt: z.date().nullable().optional(), password: z.string().nullable().optional(), - encryptedSecret: zodBuffer.nullable().optional() + encryptedSecret: zodBuffer.nullable().optional(), + id: z.string() }); export type TSecretSharing = z.infer; diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 0c021fc53..214993138 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -55,7 +55,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }, schema: { params: z.object({ - id: z.string().uuid() + id: z.string() }), body: z.object({ hashedHex: z.string().min(1), @@ -107,8 +107,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - id: z.string().uuid(), - hashedHex: z.string() + id: z.string() }) } }, @@ -117,7 +116,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => ...req.body, accessType: SecretSharingAccessType.Anyone }); - return { id: sharedSecret.id, hashedHex: sharedSecret.hashedHex }; + return { id: sharedSecret.id }; } }); @@ -138,8 +137,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - id: z.string().uuid(), - hashedHex: z.string() + id: z.string() }) } }, @@ -153,7 +151,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, ...req.body }); - return { id: sharedSecret.id, hashedHex: sharedSecret.hashedHex }; + return { id: sharedSecret.id }; } }); @@ -165,7 +163,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }, schema: { params: z.object({ - sharedSecretId: z.string().uuid() + sharedSecretId: z.string() }), response: { 200: SecretSharingSchema diff --git a/backend/src/services/secret-sharing/secret-sharing-dal.ts b/backend/src/services/secret-sharing/secret-sharing-dal.ts index 5c690b266..f6108b3ff 100644 --- a/backend/src/services/secret-sharing/secret-sharing-dal.ts +++ b/backend/src/services/secret-sharing/secret-sharing-dal.ts @@ -82,11 +82,21 @@ export const secretSharingDALFactory = (db: TDbClient) => { } }; + const create = async (data: Omit, tx?: Knex) => { + try { + const [res] = await (tx || db)(TableName.SecretSharing).insert(data).returning("*"); + return res; + } catch (error) { + throw new DatabaseError({ error, name: "Create Shared Secret" }); + } + }; + return { ...sharedSecretOrm, countAllUserOrgSharedSecrets, pruneExpiredSharedSecrets, softDeleteById, - findActiveSharedSecrets + findActiveSharedSecrets, + create }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 4ad6124fb..0aa89dda1 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -68,10 +68,15 @@ export const secretSharingServiceFactory = ({ const encryptWithRoot = kmsService.encryptWithRootKey(); const encryptedSecret = encryptWithRoot(Buffer.from(secretValue)); - const hashedHex = crypto.createHash("sha256").update(secretValue).digest("hex").substring(0, 13); + + // This will be 36 characters long, due to encoding it to base64. + const id = crypto.randomBytes(27).toString("base64url"); + + const hashedHex = crypto.createHash("sha256").update(id).digest("base64").substring(0, 13); const hashedPassword = password ? await bcrypt.hash(password, 10) : null; const newSharedSecret = await secretSharingDAL.create({ + id, iv: null, tag: null, encryptedValue: null, @@ -86,7 +91,7 @@ export const secretSharingServiceFactory = ({ accessType }); - return { id: newSharedSecret.id, hashedHex: newSharedSecret.hashedHex }; + return { id: `${newSharedSecret.id}${hashedHex}` }; }; const createPublicSharedSecret = async ({ @@ -116,10 +121,12 @@ export const secretSharingServiceFactory = ({ const encryptWithRoot = kmsService.encryptWithRootKey(); const encryptedSecret = encryptWithRoot(Buffer.from(secretValue)); - const hashedHex = crypto.createHash("sha256").update(secretValue).digest("hex").substring(0, 13); + const id = crypto.randomBytes(27).toString("base64url"); + const hashedHex = crypto.createHash("sha256").update(id).digest("hex").substring(0, 13); const hashedPassword = password ? await bcrypt.hash(password, 10) : null; const newSharedSecret = await secretSharingDAL.create({ + id, encryptedValue: null, iv: null, tag: null, @@ -132,7 +139,7 @@ export const secretSharingServiceFactory = ({ accessType }); - return { id: newSharedSecret.id, hashedHex: newSharedSecret.hashedHex }; + return { id: `${newSharedSecret.id}${hashedHex}` }; }; const getSharedSecrets = async ({ diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index 4f0b27d95..b9843a711 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -15,7 +15,6 @@ export type TSharedSecret = { export type TCreatedSharedSecret = { id: string; - hashedHex: string; }; export type TCreateSharedSecretRequest = { diff --git a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx index 91cd36257..6aa44f65d 100644 --- a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx +++ b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx @@ -76,7 +76,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { try { const expiresAt = new Date(new Date().getTime() + Number(expiresIn)); - const { id, hashedHex } = await createSharedSecret.mutateAsync({ + const { id } = await createSharedSecret.mutateAsync({ name, password, secretValue: secret, @@ -85,7 +85,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { accessType }); - setSecretLink(`${window.location.origin}/shared/secret/${id}-${hashedHex}`); + setSecretLink(`${window.location.origin}/shared/secret/${id}`); reset(); setCopyTextSecret("secret"); diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index 6d75ca782..f2309860b 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -13,23 +13,33 @@ import { PasswordContainer, SecretContainer, SecretErrorContainer } from "./comp const extractDetailsFromUrl = (router: NextRouter) => { const { id, key: urlEncodedKey } = router.query; + const idString = id as string; + + if (!idString) { + return { + id: "", + hashedHex: "", + key: null + }; + } + if (urlEncodedKey) { const [hashedHex, key] = urlEncodedKey ? urlEncodedKey.toString().split("-") : ["", ""]; return { - id: id as string, + id: idString, hashedHex, key }; } - // its like this {uuid}-{hex} so example: idpart1-idpart2-idpart3-idpart4-hex - const extractedId = id?.toString().split("-").slice(0, 5).join("-"); - const extractedHex = id?.toString().split("-").slice(5).join("-"); + // get the first 36 characters as id and the rest as hex + const idPart = idString.substring(0, 36); + const hexPart = idString.substring(36); return { - id: extractedId || "", - hashedHex: extractedHex || "", + id: idPart || "", + hashedHex: hexPart || "", key: null }; };