Skip to content

Commit b5c1cfb

Browse files
committed
fix(upload): use a v4 UUID with an insecure-context fallback
crypto.randomUUID() is only exposed in secure contexts, so Talk served over plain HTTP threw "crypto.randomUUID is not a function" when building a temporary upload name. Add a randomUuid() helper that uses crypto.randomUUID() when available and otherwise generates an RFC 4122 v4 UUID from crypto.getRandomValues() (available over HTTP), and use it at the upload call site. Fixes #18733. Signed-off-by: youdie006 <youdie006@users.noreply.github.com>
1 parent 2bc128e commit b5c1cfb

3 files changed

Lines changed: 90 additions & 1 deletion

File tree

src/stores/upload.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
separateDuplicateUploads,
3737
} from '../utils/fileUpload.ts'
3838
import { parseUploadError } from '../utils/propfindErrorParse.ts'
39+
import { randomUuid } from '../utils/randomUuid.ts'
3940
import { useActorStore } from './actor.ts'
4041
import { useChatExtrasStore } from './chatExtras.ts'
4142
import { useSettingsStore } from './settings.ts'
@@ -469,7 +470,7 @@ export const useUploadStore = defineStore('upload', () => {
469470
// resolves the final name (and any conflicts) when postAttachment
470471
// moves the file out of Draft.
471472
for (const [index] of getInitialisedUploads(uploadId)) {
472-
const tempName = crypto.randomUUID()
473+
const tempName = randomUuid()
473474
markFileAsPendingUpload({ uploadId, index, sharePath: '/' + draftFolderPath + '/' + tempName })
474475
}
475476
return

src/utils/randomUuid.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { afterEach, describe, expect, test, vi } from 'vitest'
7+
import { randomUuid } from './randomUuid.ts'
8+
9+
// Canonical RFC 4122 version 4 UUID: version nibble is 4, variant nibble is 8/9/a/b
10+
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
11+
12+
describe('randomUuid', () => {
13+
afterEach(() => {
14+
vi.restoreAllMocks()
15+
})
16+
17+
test('returns a valid v4 UUID when crypto.randomUUID is available', () => {
18+
expect(typeof crypto.randomUUID).toBe('function')
19+
expect(randomUuid()).toMatch(UUID_V4_REGEX)
20+
})
21+
22+
test('falls back to getRandomValues when crypto.randomUUID is undefined', () => {
23+
const originalRandomUUID = crypto.randomUUID
24+
try {
25+
// Simulate an insecure context (plain HTTP), where randomUUID is missing
26+
// @ts-expect-error - emulate the insecure-context runtime where the method is absent
27+
crypto.randomUUID = undefined
28+
29+
const getRandomValuesSpy = vi.spyOn(crypto, 'getRandomValues')
30+
31+
const uuid = randomUuid()
32+
33+
expect(getRandomValuesSpy).toHaveBeenCalled()
34+
expect(uuid).toMatch(UUID_V4_REGEX)
35+
} finally {
36+
crypto.randomUUID = originalRandomUUID
37+
}
38+
})
39+
40+
test('generates unique values on repeated calls', () => {
41+
const values = new Set(Array.from({ length: 100 }, () => randomUuid()))
42+
expect(values.size).toBe(100)
43+
})
44+
})

src/utils/randomUuid.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
/**
7+
* Generate an RFC 4122 version 4 (random) UUID.
8+
*
9+
* `crypto.randomUUID()` is only exposed in secure contexts (HTTPS or
10+
* localhost). When Talk is served over plain HTTP it is `undefined`, which
11+
* makes `crypto.randomUUID()` throw. In that case we fall back to
12+
* `crypto.getRandomValues()`, which is available in insecure contexts too,
13+
* and assemble the UUID manually.
14+
*
15+
* @return a random UUID in the canonical `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` form
16+
*/
17+
function randomUuid(): string {
18+
if (typeof crypto.randomUUID === 'function') {
19+
return crypto.randomUUID()
20+
}
21+
22+
// Fallback for insecure contexts (plain HTTP): crypto.randomUUID is not
23+
// available, but crypto.getRandomValues still is.
24+
const bytes = crypto.getRandomValues(new Uint8Array(16))
25+
26+
// Set the version to 4 (0100) in the high nibble of byte 6
27+
bytes[6] = (bytes[6] & 0x0f) | 0x40
28+
// Set the variant to RFC 4122 (10xx) in the two high bits of byte 8
29+
bytes[8] = (bytes[8] & 0x3f) | 0x80
30+
31+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0'))
32+
33+
return [
34+
hex.slice(0, 4).join(''),
35+
hex.slice(4, 6).join(''),
36+
hex.slice(6, 8).join(''),
37+
hex.slice(8, 10).join(''),
38+
hex.slice(10, 16).join(''),
39+
].join('-')
40+
}
41+
42+
export {
43+
randomUuid,
44+
}

0 commit comments

Comments
 (0)