feat(share): add share ID length setting (#677)

- Add share ID length to share > settings
- Use cryptographically secure RNG for IDs
- Use secure default value for IDs length
- Add FR and EN translation

Co-authored-by: Romain Ricard <romain.ricard@mines-ales.org>
This commit is contained in:
Romain Ricard
2024-11-24 17:25:50 +01:00
committed by GitHub
parent f78ffd69e7
commit 9d4bb55a09
5 changed files with 45 additions and 13 deletions

View File

@@ -51,6 +51,11 @@ const configVariables: ConfigVariables = {
defaultValue: "0", defaultValue: "0",
secret: false, secret: false,
}, },
shareIdLength: {
type: "number",
defaultValue: "8",
secret: false,
},
maxSize: { maxSize: {
type: "number", type: "number",
defaultValue: "1000000000", defaultValue: "1000000000",

View File

@@ -40,6 +40,7 @@ const showCreateUploadModal = (
allowUnauthenticatedShares: boolean; allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean; enableEmailRecepients: boolean;
maxExpirationInHours: number; maxExpirationInHours: number;
shareIdLength: number;
simplified: boolean; simplified: boolean;
}, },
files: FileUpload[], files: FileUpload[],
@@ -72,18 +73,28 @@ const showCreateUploadModal = (
}); });
}; };
const generateLink = () => const generateShareId = (length: number = 16) => {
Buffer.from(Math.random().toString(), "utf8") const chars =
.toString("base64") "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
.substring(10, 17); let result = "";
const randomArray = new Uint8Array(length >= 3 ? length : 3);
crypto.getRandomValues(randomArray);
randomArray.forEach((number) => {
result += chars[number % chars.length];
});
return result;
};
const generateAvailableLink = async (times = 10): Promise<string> => { const generateAvailableLink = async (
shareIdLength: number,
times: number = 10,
): Promise<string> => {
if (times <= 0) { if (times <= 0) {
throw new Error("Could not generate available link"); throw new Error("Could not generate available link");
} }
const _link = generateLink(); const _link = generateShareId(shareIdLength);
if (!(await shareService.isShareIdAvailable(_link))) { if (!(await shareService.isShareIdAvailable(_link))) {
return await generateAvailableLink(times - 1); return await generateAvailableLink(shareIdLength, times - 1);
} else { } else {
return _link; return _link;
} }
@@ -102,12 +113,13 @@ const CreateUploadModalBody = ({
allowUnauthenticatedShares: boolean; allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean; enableEmailRecepients: boolean;
maxExpirationInHours: number; maxExpirationInHours: number;
shareIdLength: number;
}; };
}) => { }) => {
const modals = useModals(); const modals = useModals();
const t = useTranslate(); const t = useTranslate();
const generatedLink = generateLink(); const generatedLink = generateShareId(options.shareIdLength);
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true); const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
@@ -229,7 +241,12 @@ const CreateUploadModalBody = ({
<Button <Button
style={{ flex: "0 0 auto" }} style={{ flex: "0 0 auto" }}
variant="outline" variant="outline"
onClick={() => form.setFieldValue("link", generateLink())} onClick={() =>
form.setFieldValue(
"link",
generateShareId(options.shareIdLength),
)
}
> >
<FormattedMessage id="common.button.generate" /> <FormattedMessage id="common.button.generate" />
</Button> </Button>
@@ -461,6 +478,7 @@ const SimplifiedCreateUploadModalModal = ({
allowUnauthenticatedShares: boolean; allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean; enableEmailRecepients: boolean;
maxExpirationInHours: number; maxExpirationInHours: number;
shareIdLength: number;
}; };
}) => { }) => {
const modals = useModals(); const modals = useModals();
@@ -485,10 +503,12 @@ const SimplifiedCreateUploadModalModal = ({
}); });
const onSubmit = form.onSubmit(async (values) => { const onSubmit = form.onSubmit(async (values) => {
const link = await generateAvailableLink().catch(() => { const link = await generateAvailableLink(options.shareIdLength).catch(
() => {
toast.error(t("upload.modal.link.error.taken")); toast.error(t("upload.modal.link.error.taken"));
return undefined; return undefined;
}); },
);
if (!link) { if (!link) {
return; return;

View File

@@ -468,6 +468,9 @@ export default {
"admin.config.share.max-expiration": "Max expiration", "admin.config.share.max-expiration": "Max expiration",
"admin.config.share.max-expiration.description": "admin.config.share.max-expiration.description":
"Maximum share expiration in hours. Set to 0 to allow unlimited expiration.", "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
"admin.config.share.share-id-length": "Default share ID length",
"admin.config.share.share-id-length.description":
"Default length for the generated ID of a share. This value is also used to generate links for reverse shares. A value below 8 is not considered secure.",
"admin.config.share.max-size": "Max size", "admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes", "admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.share.zip-compression-level": "Zip compression level", "admin.config.share.zip-compression-level": "Zip compression level",

View File

@@ -336,6 +336,9 @@ export default {
"admin.config.share.allow-unauthenticated-shares.description": "Permet aux visiteurs de créer des partages", "admin.config.share.allow-unauthenticated-shares.description": "Permet aux visiteurs de créer des partages",
"admin.config.share.max-expiration": "Échéance", "admin.config.share.max-expiration": "Échéance",
"admin.config.share.max-expiration.description": "Échéance du partage en heures. Indiquez 0 pour quil nexpire jamais.", "admin.config.share.max-expiration.description": "Échéance du partage en heures. Indiquez 0 pour quil nexpire jamais.",
"admin.config.share.share-id-length": "Taille de l'identifiant généré",
"admin.config.share.share-id-length.description":
"Taille par défaut de l'identifiant généré pour un partage. Cette valeur est aussi utilisée pour générer les liens des partages inverses. Une valeur inférieure à 8 n'est pas considérée sûre.",
"admin.config.share.max-size": "Taille max", "admin.config.share.max-size": "Taille max",
"admin.config.share.max-size.description": "Taille maximale du partage en octets", "admin.config.share.max-size.description": "Taille maximale du partage en octets",
"admin.config.share.zip-compression-level": "Niveau de compression", "admin.config.share.zip-compression-level": "Niveau de compression",

View File

@@ -140,6 +140,7 @@ const Upload = ({
), ),
enableEmailRecepients: config.get("email.enableShareEmailRecipients"), enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
maxExpirationInHours: config.get("share.maxExpiration"), maxExpirationInHours: config.get("share.maxExpiration"),
shareIdLength: config.get("share.shareIdLength"),
simplified, simplified,
}, },
files, files,