diff --git a/components/access-keys/new-item.tsx b/components/access-keys/new-item.tsx index 256a0903..21abed88 100644 --- a/components/access-keys/new-item.tsx +++ b/components/access-keys/new-item.tsx @@ -15,6 +15,7 @@ import { useAccessKeys } from "@/hooks/use-access-keys" import { useApi } from "@/contexts/api-context" import { makeRandomString } from "@/lib/functions" import { cn } from "@/lib/utils" +import { isSecretKeyValid } from "@/lib/secret-key" interface AccessKeysNewItemProps { visible: boolean @@ -105,8 +106,8 @@ export function AccessKeysNewItem({ visible, onVisibleChange, onSuccess, onNotic } if (!secretKey) { newErrors.secretKey = t("Please enter Secret Key") - } else if (secretKey.length < 8 || secretKey.length > 40) { - newErrors.secretKey = t("Secret Key length must be between 8 and 40 characters") + } else if (!isSecretKeyValid(secretKey)) { + newErrors.secretKey = t("Secret Key must be at least 8 bytes") } if (!name) { newErrors.name = t("Please enter name") diff --git a/components/account/change-password-dialog.tsx b/components/account/change-password-dialog.tsx index 72080ae7..ef6599e0 100644 --- a/components/account/change-password-dialog.tsx +++ b/components/account/change-password-dialog.tsx @@ -10,9 +10,7 @@ import { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from "@ import { Alert, AlertDescription } from "@/components/ui/alert" import { useAccount } from "@/hooks/use-account" import { useMessage } from "@/lib/feedback/message" - -/** Mirrors the server's `SECRET_KEY_MIN_LEN`. */ -const PASSWORD_MIN_LENGTH = 8 +import { isSecretKeyValid } from "@/lib/secret-key" interface ChangePasswordDialogProps { open: boolean @@ -66,8 +64,8 @@ export function ChangePasswordDialog({ open, onOpenChange, onChanged }: ChangePa current: current ? "" : t("Please enter your current password"), next: !next ? t("Please enter new password") - : next.length < PASSWORD_MIN_LENGTH - ? t("Password must be at least 8 characters") + : !isSecretKeyValid(next) + ? t("Password must be at least 8 bytes") : next === current ? t("The new password must be different from the current one") : "", @@ -164,7 +162,6 @@ export function ChangePasswordDialog({ open, onOpenChange, onChanged }: ChangePa type="password" autoComplete="new-password" spellCheck={false} - minLength={PASSWORD_MIN_LENGTH} required disabled={submitting} aria-invalid={Boolean(errors.next)} @@ -172,7 +169,7 @@ export function ChangePasswordDialog({ open, onOpenChange, onChanged }: ChangePa /> - {t("At least 8 characters. This is also your S3 secret key.")} + {t("At least 8 bytes. This is also your S3 secret key.")} {errors.next} diff --git a/components/user/edit-form.tsx b/components/user/edit-form.tsx index 38cd29ad..e8a1e445 100644 --- a/components/user/edit-form.tsx +++ b/components/user/edit-form.tsx @@ -13,6 +13,7 @@ import { usePermissions } from "@/hooks/use-permissions" import { usePolicies } from "@/hooks/use-policies" import { useGroups } from "@/hooks/use-groups" import { useMessage } from "@/lib/feedback/message" +import { isSecretKeyValid } from "@/lib/secret-key" import { getAvailableUserEditTabs } from "@/lib/user-edit-tabs" import { UserEditSecretKey } from "./edit/secret-key" import { UserEditGroups } from "./edit/groups" @@ -177,8 +178,8 @@ export function UserEditForm({ open, onOpenChange, row, onSuccess }: UserEditFor const validate = () => { const newErrors = { secretKey: "" } - if (secretKey && !/^.{8,40}$/.test(secretKey)) { - newErrors.secretKey = t("password length cannot be less than 8 characters and greater than 40 characters") + if (secretKey && !isSecretKeyValid(secretKey)) { + newErrors.secretKey = t("Password must be at least 8 bytes") } setErrors(newErrors) return !newErrors.secretKey diff --git a/components/user/edit/access-keys.tsx b/components/user/edit/access-keys.tsx index b8e11a63..627b6cbc 100644 --- a/components/user/edit/access-keys.tsx +++ b/components/user/edit/access-keys.tsx @@ -24,6 +24,7 @@ import { useDialog } from "@/lib/feedback/dialog" import { useMessage } from "@/lib/feedback/message" import { formatDateTime, makeRandomString } from "@/lib/functions" import { cn } from "@/lib/utils" +import { isSecretKeyValid } from "@/lib/secret-key" import { useTranslation } from "react-i18next" import type { ColumnDef } from "@tanstack/react-table" @@ -169,8 +170,8 @@ function UserAccessKeysNewDialog({ open, onOpenChange, userName, onSuccess, onNo if (!secretKey) { nextErrors.secretKey = t("Please enter Secret Key") - } else if (secretKey.length < 8 || secretKey.length > 40) { - nextErrors.secretKey = t("Secret Key length must be between 8 and 40 characters") + } else if (!isSecretKeyValid(secretKey)) { + nextErrors.secretKey = t("Secret Key must be at least 8 bytes") } if (!name) { diff --git a/components/user/new-form.tsx b/components/user/new-form.tsx index 227608e2..2006f4fd 100644 --- a/components/user/new-form.tsx +++ b/components/user/new-form.tsx @@ -16,6 +16,7 @@ import { useUsers } from "@/hooks/use-users" import { usePolicies } from "@/hooks/use-policies" import { useGroups } from "@/hooks/use-groups" import { cn } from "@/lib/utils" +import { isSecretKeyValid } from "@/lib/secret-key" interface UserNewFormProps { open: boolean @@ -25,8 +26,6 @@ interface UserNewFormProps { const USERNAME_MIN_LENGTH = 3 const USERNAME_MAX_LENGTH = 128 -const PASSWORD_MIN_LENGTH = 8 -const PASSWORD_MAX_LENGTH = 40 export function UserNewForm({ open, onOpenChange, onSuccess }: UserNewFormProps) { const { t } = useTranslation() @@ -115,8 +114,8 @@ export function UserNewForm({ open, onOpenChange, onSuccess }: UserNewFormProps) } if (!secretKey.trim()) { newErrors.secretKey = t("Please enter password") - } else if (secretKey.length < PASSWORD_MIN_LENGTH || secretKey.length > PASSWORD_MAX_LENGTH) { - newErrors.secretKey = t("password length cannot be less than 8 characters and greater than 40 characters") + } else if (!isSecretKeyValid(secretKey)) { + newErrors.secretKey = t("Password must be at least 8 bytes") } setErrors(newErrors) return !newErrors.accessKey && !newErrors.secretKey @@ -218,8 +217,6 @@ export function UserNewForm({ open, onOpenChange, onSuccess }: UserNewFormProps) value={secretKey} onChange={(e) => setSecretKey(e.target.value)} type="password" - minLength={PASSWORD_MIN_LENGTH} - maxLength={PASSWORD_MAX_LENGTH} autoComplete="new-password" spellCheck={false} required diff --git a/i18n/locales/ar-MA.json b/i18n/locales/ar-MA.json index c98bdb10..089f2e1f 100644 --- a/i18n/locales/ar-MA.json +++ b/i18n/locales/ar-MA.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "يجب أن تتكون كلمة المرور من 8 بايتات على الأقل", + "Secret Key must be at least 8 bytes": "يجب أن يتكون المفتاح السري من 8 بايتات على الأقل", + "At least 8 bytes. This is also your S3 secret key.": "8 بايتات على الأقل. هذا أيضًا مفتاح S3 السري الخاص بك." } diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index 380c551f..46985f25 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "Das Passwort muss mindestens 8 Bytes lang sein", + "Secret Key must be at least 8 bytes": "Der geheime Schlüssel muss mindestens 8 Bytes lang sein", + "At least 8 bytes. This is also your S3 secret key.": "Mindestens 8 Bytes. Dies ist auch Ihr geheimer S3-Schlüssel." } diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index 90e410b9..e3666bab 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "Password must be at least 8 bytes", + "Secret Key must be at least 8 bytes": "Secret Key must be at least 8 bytes", + "At least 8 bytes. This is also your S3 secret key.": "At least 8 bytes. This is also your S3 secret key." } diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index 54fbe3fe..5d7d4865 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "La contraseña debe tener al menos 8 bytes", + "Secret Key must be at least 8 bytes": "La clave secreta debe tener al menos 8 bytes", + "At least 8 bytes. This is also your S3 secret key.": "Al menos 8 bytes. Esta también es su clave secreta de S3." } diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 0426e82f..5f5a8c82 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "Le mot de passe doit contenir au moins 8 octets", + "Secret Key must be at least 8 bytes": "La clé secrète doit contenir au moins 8 octets", + "At least 8 bytes. This is also your S3 secret key.": "Au moins 8 octets. Il s’agit aussi de votre clé secrète S3." } diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index f3ac36d5..76ada96b 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "Kata sandi harus berisi minimal 8 byte", + "Secret Key must be at least 8 bytes": "Kunci rahasia harus berisi minimal 8 byte", + "At least 8 bytes. This is also your S3 secret key.": "Minimal 8 byte. Ini juga merupakan kunci rahasia S3 Anda." } diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index fa30363e..00c95fd3 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "La password deve contenere almeno 8 byte", + "Secret Key must be at least 8 bytes": "La chiave segreta deve contenere almeno 8 byte", + "At least 8 bytes. This is also your S3 secret key.": "Almeno 8 byte. Questa è anche la chiave segreta S3." } diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index 0d0c6dad..d57ab562 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "パスワードは8バイト以上必要です", + "Secret Key must be at least 8 bytes": "シークレットキーは8バイト以上必要です", + "At least 8 bytes. This is also your S3 secret key.": "8バイト以上。これはS3シークレットキーでもあります。" } diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index 90285d62..d0340644 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "비밀번호는 최소 8바이트여야 합니다", + "Secret Key must be at least 8 bytes": "시크릿 키는 최소 8바이트여야 합니다", + "At least 8 bytes. This is also your S3 secret key.": "최소 8바이트입니다. S3 시크릿 키로도 사용됩니다." } diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index 679cde46..de99ad1b 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "A senha deve ter pelo menos 8 bytes", + "Secret Key must be at least 8 bytes": "A chave secreta deve ter pelo menos 8 bytes", + "At least 8 bytes. This is also your S3 secret key.": "Pelo menos 8 bytes. Esta também é sua chave secreta S3." } diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index 46b0e6e3..9eec445c 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "Пароль должен содержать не менее 8 байт", + "Secret Key must be at least 8 bytes": "Секретный ключ должен содержать не менее 8 байт", + "At least 8 bytes. This is also your S3 secret key.": "Не менее 8 байт. Это также ваш секретный ключ S3." } diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index d0a96fc8..3397b877 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "Parola en az 8 bayt olmalıdır", + "Secret Key must be at least 8 bytes": "Gizli anahtar en az 8 bayt olmalıdır", + "At least 8 bytes. This is also your S3 secret key.": "En az 8 bayt. Bu aynı zamanda S3 gizli anahtarınızdır." } diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index ba2a07b6..94333697 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "You do not have permission to control backfill", "You do not have permission to view this bucket's migration settings.": "You do not have permission to view this bucket's migration settings.", "Your migration configuration changes will be lost.": "Your migration configuration changes will be lost.", - "pull failures": "pull failures" + "pull failures": "pull failures", + "Password must be at least 8 bytes": "Mật khẩu phải có ít nhất 8 byte", + "Secret Key must be at least 8 bytes": "Khóa bí mật phải có ít nhất 8 byte", + "At least 8 bytes. This is also your S3 secret key.": "Ít nhất 8 byte. Đây cũng là khóa bí mật S3 của bạn." } diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index d900377f..cf343376 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -2226,5 +2226,8 @@ "You do not have permission to control backfill": "你没有控制回填任务的权限", "You do not have permission to view this bucket's migration settings.": "你没有查看此存储桶迁移设置的权限。", "Your migration configuration changes will be lost.": "对迁移配置的更改将会丢失。", - "pull failures": "次拉取失败" + "pull failures": "次拉取失败", + "Password must be at least 8 bytes": "密码至少需要 8 个字节", + "Secret Key must be at least 8 bytes": "Secret Key 至少需要 8 个字节", + "At least 8 bytes. This is also your S3 secret key.": "至少 8 个字节。这也是您的 S3 Secret Key。" } diff --git a/lib/secret-key.ts b/lib/secret-key.ts new file mode 100644 index 00000000..d8dc013e --- /dev/null +++ b/lib/secret-key.ts @@ -0,0 +1,4 @@ +/** Matches the server's Secret Key length validation without changing the value. */ +export function isSecretKeyValid(secretKey: string): boolean { + return new TextEncoder().encode(secretKey).length >= 8 +} diff --git a/tests/lib/iam-remaining-safety.test.js b/tests/lib/iam-remaining-safety.test.js index 0147e226..bc2b0a49 100644 --- a/tests/lib/iam-remaining-safety.test.js +++ b/tests/lib/iam-remaining-safety.test.js @@ -27,7 +27,7 @@ test("change password proves knowledge of the current secret before rotating it" // change the account's credentials. assert.match(source, /changePassword\(current, next\)/) assert.match(source, /autoComplete="current-password"/) - assert.match(source, /PASSWORD_MIN_LENGTH = 8/) + assert.match(source, /!isSecretKeyValid\(next\)/) assert.match(source, / { + for (const secret of ["", "1234567", "ééé"]) { + assert.equal(isSecretKeyValid(secret), false) + } + for (const secret of ["12345678", "éééé", "🔑🔑"]) { + assert.equal(isSecretKeyValid(secret), true) + } +}) + +test("custom secret keys have no 40 or 256 byte business limit", () => { + for (const length of [40, 41, 90, 128, 256, 257, 4096]) { + assert.equal(isSecretKeyValid("s".repeat(length)), true, `${length}-byte secret`) + } +}) + +test("length validation preserves whitespace and Unicode without normalization", () => { + for (const secret of [" 123456 ", "éééé", "e\u0301e\u0301e\u0301"]) { + assert.equal(isSecretKeyValid(secret), true) + } +})