Skip to content

Commit 7604729

Browse files
authored
Encryption at rest: operator surface (P3A) (#7501)
# Encryption at rest: operator surface (P3A) Encryption at rest has shipped twice and has never been visible in the product. P1 (#7155) built the crypto, P2 (#7173) built the admin API. Today an operator enables it by editing YAML, revokes a key with curl, and has no screen anywhere telling them the feature exists or whether their files are actually encrypted. This PR puts the P2 API behind a UI. No crypto changes, no schema changes. ## Scope In: the Storage tab re-enabled with encryption as its content, key actions, migration control, rotation status, the encrypted badge on documents, and honest licence messaging. Note on placement: #7497 removed the Infrastructure tabs backed by mock-only `/v1/infrastructure/*` data, including the old Storage tab, leaving `storage` as a disabled placeholder. This PR re-enables it, because it now reads the real `/api/v1/admin/storage-encryption` surface, which is the criterion #7497 used. The mock usage, quota, provider and retention UI that used to live there is not restored. Out: per-source encryption (P3B, needs a schema change), KMS and BYOK backends (P4), FIPS builds (P4). ## User stories ### Knowing where you stand **US-1. See whether encryption is actually on** As an administrator, I want one screen that tells me whether stored files are being encrypted, so that I can answer "are we encrypted at rest?" without reading config files or logs. - [x] The Storage tab under Infrastructure is enabled again, with encryption as its content - [x] Shows write state from `writeEnabled`: new uploads are encrypted, or they are not - [x] Shows `encryptedFiles` and `plaintextFiles` counts, so partial coverage is obvious - [x] Distinguishes `writeEnabled: false, active: true` (decrypt-only, the state after turning the flag off) from `writeEnabled: false, active: false` (feature never used) - [x] Empty state when no keys exist yet: explains that a key is created on the first upload, not at startup - [x] When storage is disabled the API returns 403 "Storage is disabled" before touching the database. The panel shows that as an explanatory state, not an error toast - [x] When the key registry cannot be read the API returns 503. The panel says the registry is unavailable and suggests checking the database, rather than showing zero keys **US-2. Verify my key backup matches the running system** As an administrator, I want to compare the live master key against my backup without exposing key material, so that I can prove my disaster recovery actually works before I need it. - [x] Displays `masterKeyFingerprint` (SHA-256 prefix, 16 hex characters) and `masterKeyVersion` - [x] Copy-to-clipboard on the fingerprint - [x] Explains in one line what the fingerprint is for: it matches the fingerprint logged at startup, so an operator can check their archived key is the one in use - [x] Never renders anything that could be key material, and the field is absent when the key machinery has not been materialised **US-3. Be told when my licence gives me no audit trail** As a compliance reviewer on a Pro licence, I want the product to tell me that encryption events are not being recorded, so that I do not report an audit trail to my auditor that does not exist. - [x] On a non-Enterprise licence the panel carries a persistent notice: encryption is active, audit events are not recorded, this requires Enterprise - [x] The notice sits next to any claim about auditing, not in a separate help page - [x] On Enterprise the notice is absent - [x] Wording matches the startup warning and `devGuide/STORAGE_ENCRYPTION_AT_REST.md`, so the log, the docs and the UI agree Note: encryption is gated at Pro but `AuditService` only records on Enterprise. Today this is visible only in a startup log line. If we would rather move audit down to Pro, that is a licence decision and this story changes to "remove the notice". **US-4. Be told my master key was generated for me** As an administrator of a single-node install, I want to know that the system created an encryption key on my behalf, so that I do not discover an unbacked-up secret after losing it. - [x] When the key came from the auto-generated `file-encryption.key` file rather than explicit config, the panel says so and states the consequence: lose this file and encrypted files cannot be recovered - [x] Links to the backup section of the runbook - [x] Requires a small `/status` addition to report key provenance (config, environment, or generated file). Included in this PR ### Acting on it **US-5. Revoke a scope's stored content** As an administrator responding to an incident, I want to revoke access to one team's stored files, so that their content cannot be read while I investigate. - [x] Each key row has a Revoke action, calling `POST /keys/{keyId}/disable` - [x] The confirmation states what revocation actually does, in plain words: - reads of existing files under this key start failing with 403 - the team can still upload new files, which get a fresh key - it is reversible, and no key material is destroyed - on a cluster, other nodes take up to 60 seconds to catch up - [x] The row shows DISABLED, plus `statusChangedBy` and `statusChangedAt` - [x] The 60 second cluster note is only shown when clustering is on Note: the "still uploads" and "60 seconds" points are not padding. Both are real behaviour that surprised us during review, and an admin who believes revoke means "frozen instantly" will be wrong. **US-6. Restore access, and understand what came back** As an administrator, I want to undo a revocation and see exactly what state the key returned to, so that I am not misled into thinking a key is wrapping new files when it is not. - [x] Enable action on DISABLED keys only, calling `POST /keys/{keyId}/enable` - [x] The response status is what the row shows: ACTIVE if the scope had no other active key, RETIRED if one was minted while it was revoked - [x] When the result is RETIRED, the UI explains it: this key decrypts its existing files again, and a newer key is wrapping new uploads - [x] Enable is not offered on ACTIVE or RETIRED keys, matching the API's 409 - [x] A key that no longer exists returns 404 and surfaces as a clear message, not a crash **US-7. Encrypt the files I already had** As an administrator who has just enabled encryption, I want to encrypt the existing plaintext backlog and watch it happen, so that "encrypted at rest" is true of my whole estate rather than only new uploads. - [x] Start action calls `POST /migrate`, disabled when `plaintextFiles` is zero - [x] Progress from `GET /migrate/status`: state, total, processed, skipped, failed, started time - [x] All four terminal states render distinctly: IDLE, RUNNING, COMPLETED, FAILED - [x] FAILED is a real, explained state, not a stalled spinner. The run now ends FAILED when the write flag is turned off mid-run, and the message says so and what to do - [x] Skipped is explained on hover: a user replaced the file mid-migration, so the job left their copy alone - [x] 409 when a run is already going, or when encryption is off, surfaces as a message rather than a failure - [x] Progress is in-memory, so a backend restart resets it to IDLE. The UI says that rather than appearing to lose the run - [ ] Adds a cancel endpoint and page-size and pause knobs to the API, since exposing a long job in a UI without a stop button is not defensible **US-8. Finish a key rotation without sealing my files** As an administrator rotating the master key, I want to see how many key rows are still on the old key, so that I do not remove the outgoing key while rows still depend on it. - [x] Shows the current `masterKeyVersion` and the number of key rows below it - [x] Re-wrap action calls `POST /master/rotate` and reports the `rewrapped` count - [x] States plainly that key material never travels over HTTP: the new key is a config and restart operation, and this button only performs the re-wrap step - [x] Warns while any row is behind: removing the previous key now would make those files unreadable - [x] Links to the rotation runbook ### Everyone else **US-9. See which of my files are encrypted** — moved to #7550 The badge component was built here but never wired to a surface, and no documents endpoint returns `encryptionKeyId`, so it could not render in the product. It moved to its own draft PR rather than shipping as unreachable code behind a screenshot. **US-10. Understand why a file will not open** As a user whose team's key has been revoked, I want a clear message rather than a generic error, so that I raise the right request with the right person. - [ ] A 403 from a revoked key renders as "access to this file has been revoked" wording, not a generic failure toast - [ ] Distinguished from a permissions 403 - [ ] Applies to both My Files and workflow document reads, which P2 made consistent ## Definition of done - [x] New API client module under `portal/api/`, following the conventions in `sources.ts` - [x] All copy is i18n keys in the editor `en-US` catalogue, which is the source of truth - [x] Storybook stories for every state named above, including the empty, 403, 503 and FAILED states - [x] a11y baseline recorded with the full task, so a partial scan does not delete other entries - [x] Light and dark themes both checked - [x] No raw `<button>` elements, per the lint rule - [ ] `/status` paginates its key list, since this UI is the consumer that makes an unbounded list matter ## Not covered in this PR The five unticked boxes above, all tracked elsewhere. Everything else was checked against the code, not assumed. **Split into follow-up PRs:** - **#7550** — the encrypted badge on documents (US-9). Needs a documents endpoint to return `encryptionKeyId` before it can render anywhere - **#7551** — the revoked-key message (US-10), the migration cancel endpoint with page-size and pause knobs, and pagination on the `/status` key list **Also tracked:** #7549, to publish the operator docs. The two runbook links in this PR point at `devGuide/STORAGE_ENCRYPTION_AT_REST.md` until those pages exist; both are constants in `storageEncryption.ts`, so swapping them is a one-file change. ## Backend changes riding along Two fields added to `/status`. Nothing else in the backend changed: | Change | Why | |---|---| | `masterKeySource` (config, environment, generated) | US-4 cannot be built without it. The value already existed inside `FileEncryptionMasterKey.resolveKey` and was simply never returned | | `provider` (local, database, s3) | The panel names the storage backend, and warns that object-store downloads stream through the app once anything is encrypted | | `pendingRotationRows` | The rotation warning needs a whole-table count, which the returned key list cannot give | Two items from the original plan are still not done and are called out below: key-list pagination, and the migration cancel endpoint and knobs. ## Screenshots Replace each placeholder with the matching image. | | | |---|---| | Encryption off, nothing encrypted yet | <img width="2560" height="1800" alt="01-panel-encryption-off" src="https://github.com/user-attachments/assets/36ae3c9a-2c54-48b1-8837-c7c68e157961" /> | | Encryption on, healthy, key table | <img width="2560" height="2036" alt="02-panel-active" src="https://github.com/user-attachments/assets/949d930c-7e13-409d-b960-49d44e49fc21" /> | | Pro licence audit notice | <img width="2560" height="2226" alt="03-licence-notice" src="https://github.com/user-attachments/assets/f5e1b39e-c221-4e42-87c1-0f45f8ecd2a3" /> | | Revoke confirmation | <img width="2560" height="2036" alt="04-revoke-confirm" src="https://github.com/user-attachments/assets/0c86a40c-3bf1-4a0d-aa19-c28691986e2f" /> | | Revoked key row | <img width="2560" height="1912" alt="05-key-revoked" src="https://github.com/user-attachments/assets/2333bc0d-968f-4472-b253-7877fe1d41a2" /> | | Re-enabled as RETIRED, with explanation |<img width="2560" height="1912" alt="06-key-retired" src="https://github.com/user-attachments/assets/a4e84165-020b-4377-8506-49f4312783e4" /> | | Migration running | <img width="2560" height="2222" alt="07-migration-running" src="https://github.com/user-attachments/assets/6ca07208-ad58-441d-85ba-e5617f36bb29" /> | | Migration failed after the flag was turned off | <img width="2560" height="2380" alt="08-migration-failed" src="https://github.com/user-attachments/assets/35a54549-1b92-4c17-9fad-4790bda8c24b" /> | | Rotation with rows still on the old key | <img width="2560" height="2232" alt="09-rotation-pending" src="https://github.com/user-attachments/assets/991e1811-3103-45d1-8bd6-a83b6eb9291b" /> | | Storage disabled state | <img width="2560" height="1800" alt="11-storage-disabled" src="https://github.com/user-attachments/assets/c52c5087-2a4a-4189-a3c1-22e69016fc2e" /> | | Dark theme, active panel | <img width="2560" height="2036" alt="12-panel-active-dark" src="https://github.com/user-attachments/assets/7d6c3ff5-7824-4ce6-92c7-d8af82d52c10" /> | Extras captured while iterating, not required in the body: registry unavailable <img width="2560" height="1800" alt="13-registry-unavailable" src="https://github.com/user-attachments/assets/4c8df8ce-5659-41f0-9484-010cc9f74c2d" /> (`13`), generated master key (`14`) <img width="2560" height="2226" alt="14-generated-master-key" src="https://github.com/user-attachments/assets/3ff765ed-a24f-48fa-b9b8-d92de3cc2af0" /> , dark migration (`15`) <img width="2560" height="2222" alt="15-panel-active-dark-migration" src="https://github.com/user-attachments/assets/7a2ae906-8c23-404b-b778-b40ea74c3058" /> . ## Test plan What is actually covered: - Storybook covers every state named above, driven by fixtures rather than a live backend: encryption off, active, Pro notice, revoke dialog, revoked row, restore-as-RETIRED, migration running, migration FAILED, rotation pending, storage disabled (403), registry unreadable (503), and generated master key - `Infrastructure.test.tsx` covers the tab wiring: Storage is enabled, reachable by click and by `?tab=storage`, and disabled tabs stay inert - `FileEncryptionMasterKeySourceTest` covers key provenance, including that the wire names match the values the UI switches on - The shared `Tooltip` has a play-function story asserting it opens on keyboard focus and wires `aria-describedby` - a11y scanned clean across both themes, light and dark, and the shared baseline is unchanged - Typecheck across all nine build variants, oxlint, stylelint and a production build Not covered, and worth a reviewer's attention: - No component tests for the encryption UI itself. The migration state machine and the enable-returns-RETIRED path are exercised by stories, which render them but assert almost nothing. This is the biggest remaining gap in the PR - No manual pass against a live backend with encryption on, a seeded plaintext backlog and a revoked key. Every state in this PR was driven by fixtures ## Notes for reviewers The revoke confirmation copy is deliberately blunt about two things that are easy to get wrong: revoking does not stop the team storing new files, and on a cluster it takes up to a minute. Both were found during the P2 review. If the copy reads as over-explaining, that is the intent.
1 parent 235d8fd commit 7604729

24 files changed

Lines changed: 2528 additions & 22 deletions

app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,32 @@ private StorageEncryptionStatusResponse buildStatus() {
9595
.toList();
9696
String fingerprint = null;
9797
Integer masterKeyVersion = null;
98+
String masterKeySource = null;
9899
if (encryptionState.isMaterialised()) {
99100
try {
100101
FileEncryptionKeyService keyService = encryptionState.keyService();
101102
fingerprint = keyService.masterKey().fingerprint();
102103
masterKeyVersion = keyService.masterKey().currentVersion();
104+
masterKeySource = keyService.masterKey().source().wireName();
103105
} catch (StorageEncryptionException ignored) {
104106
// Materialisation failed; status still reports counts and key rows.
105107
}
106108
}
109+
// Counted in the database so the total stays right if `keys` is ever paged.
110+
long pendingRotationRows =
111+
masterKeyVersion == null
112+
? 0
113+
: keyRepository.countByMasterKeyVersionLessThan(masterKeyVersion);
107114
return new StorageEncryptionStatusResponse(
108115
encryptionState.isWriteEnabled(),
109116
encryptionState.isMaterialised(),
110117
fingerprint,
111118
masterKeyVersion,
119+
masterKeySource,
120+
applicationProperties.getStorage().getProvider(),
112121
storedFileRepository.countByEncryptionKeyIdIsNotNull(),
113122
storedFileRepository.countByEncryptionKeyIdIsNull(),
123+
pendingRotationRows,
114124
keys);
115125
}
116126

app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/FileEncryptionMasterKey.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ public class FileEncryptionMasterKey {
4949
private final SecretKey key;
5050
private final SecretKey previousKey;
5151
private final int currentVersion;
52+
private final Source source;
53+
54+
/** Where the master key came from, so the UI can warn about an unbacked-up generated key. */
55+
public enum Source {
56+
CONFIG("config"),
57+
ENVIRONMENT("environment"),
58+
GENERATED("generated");
59+
60+
private final String wireName;
61+
62+
Source(String wireName) {
63+
this.wireName = wireName;
64+
}
65+
66+
public String wireName() {
67+
return wireName;
68+
}
69+
}
5270

5371
public FileEncryptionMasterKey(String configuredKey, boolean clusterEnabled) {
5472
this(configuredKey, null, CURRENT_VERSION, clusterEnabled);
@@ -66,7 +84,9 @@ public FileEncryptionMasterKey(
6684
String previousKeyBase64,
6785
int currentVersion,
6886
boolean clusterEnabled) {
69-
this.key = resolveKey(configuredKey, clusterEnabled);
87+
Resolved resolved = resolveKey(configuredKey, clusterEnabled);
88+
this.key = resolved.key();
89+
this.source = resolved.source();
7090
this.previousKey =
7191
previousKeyBase64 == null || previousKeyBase64.isBlank()
7292
? null
@@ -90,19 +110,27 @@ public int currentVersion() {
90110
return currentVersion;
91111
}
92112

113+
public Source source() {
114+
return source;
115+
}
116+
93117
public boolean hasPreviousKey() {
94118
return previousKey != null;
95119
}
96120

97-
private static SecretKey resolveKey(String configuredKey, boolean clusterEnabled) {
121+
private record Resolved(SecretKey key, Source source) {}
122+
123+
private static Resolved resolveKey(String configuredKey, boolean clusterEnabled) {
98124
String configured = configuredKey;
99125
String source = "stirling.security.fileEncryptionKey";
126+
Source provenance = Source.CONFIG;
100127
if (configured == null || configured.isBlank()) {
101128
configured = System.getenv("STIRLING_FILE_ENCRYPTION_KEY");
102129
source = "STIRLING_FILE_ENCRYPTION_KEY";
130+
provenance = Source.ENVIRONMENT;
103131
}
104132
if (configured != null && !configured.isBlank()) {
105-
return decodeKey(configured, source);
133+
return new Resolved(decodeKey(configured, source), provenance);
106134
}
107135
if (clusterEnabled) {
108136
throw new IllegalStateException(
@@ -111,7 +139,7 @@ private static SecretKey resolveKey(String configuredKey, boolean clusterEnabled
111139
+ " stirling.security.fileEncryptionKey) to the same value on every"
112140
+ " node.");
113141
}
114-
return loadOrCreateKeyFile();
142+
return new Resolved(loadOrCreateKeyFile(), Source.GENERATED);
115143
}
116144

117145
/**

app/proprietary/src/main/java/stirling/software/proprietary/storage/model/api/StorageEncryptionStatusResponse.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ public record StorageEncryptionStatusResponse(
1313
boolean active,
1414
String masterKeyFingerprint,
1515
Integer masterKeyVersion,
16+
/** Where the master key came from: config, environment or generated. */
17+
String masterKeySource,
18+
/** Backend serving stored blobs: local, database or s3. */
19+
String provider,
1620
long encryptedFiles,
1721
long plaintextFiles,
22+
/** Key rows still wrapped by an older master key, counted in the database. */
23+
long pendingRotationRows,
1824
List<KeyInfo> keys) {
1925

2026
public record KeyInfo(
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package stirling.software.proprietary.storage.crypto;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.Base64;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
/**
10+
* Provenance drives the "this key was generated for you" warning, which is the only prompt an
11+
* operator gets to back up a key they never chose.
12+
*/
13+
class FileEncryptionMasterKeySourceTest {
14+
15+
private static final String MASTER = Base64.getEncoder().encodeToString(new byte[32]);
16+
17+
@Test
18+
void configuredKeyReportsConfig() {
19+
FileEncryptionMasterKey key = new FileEncryptionMasterKey(MASTER, false);
20+
21+
assertThat(key.source()).isEqualTo(FileEncryptionMasterKey.Source.CONFIG);
22+
assertThat(key.source().wireName()).isEqualTo("config");
23+
}
24+
25+
@Test
26+
void wireNamesMatchTheValuesTheUiSwitchesOn() {
27+
assertThat(FileEncryptionMasterKey.Source.ENVIRONMENT.wireName()).isEqualTo("environment");
28+
assertThat(FileEncryptionMasterKey.Source.GENERATED.wireName()).isEqualTo("generated");
29+
}
30+
}

devGuide/STORAGE_ENCRYPTION_AT_REST.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ Disabling only stops encrypting *new* writes. Existing encrypted files stay read
7474
key material is present — the decrypt path is always active and is never licence-gated, so a lapsed
7575
licence cannot lock you out of your own data.
7676

77+
## Backing up the master key
78+
79+
Every scope key in `file_encryption_keys` is wrapped by the master key, so the master key is the
80+
single thing standing between a backup and unreadable files. Losing it makes every encrypted stored
81+
file unrecoverable — there is no recovery path, by design.
82+
83+
Back it up when you enable encryption, not later:
84+
85+
- **Key set in config or environment.** Store the base64 value in whatever holds your other
86+
secrets. The value in `stirling.security.fileEncryptionKey` or `STIRLING_FILE_ENCRYPTION_KEY`
87+
*is* the backup.
88+
- **Auto-generated key.** The server wrote `configs/file-encryption.key` on first use and logged a
89+
warning. Copy that file somewhere durable and off the machine. Nothing else in the install holds
90+
a copy. `/status` reports `masterKeySource: "generated"` for this case, and the admin UI shows a
91+
warning until the key is configured explicitly.
92+
93+
To check a backup matches the running system, compare its fingerprint against the one in the
94+
startup log or in `/status` — see [Status and backup verification](#status-and-backup-verification).
95+
The fingerprint is a SHA-256 prefix, so it can be compared safely without exposing key material.
96+
97+
Restoring is just putting the key back: set it in config or the environment, or restore the file to
98+
`configs/file-encryption.key`, and start the server. No re-encryption is involved.
99+
77100
## Encrypting files that already exist
78101

79102
Enabling the flag does not touch the existing plaintext backlog. To convert it:
@@ -163,9 +186,14 @@ Two semantics worth knowing when reading the trail:
163186
curl http://localhost:8080/api/v1/admin/storage-encryption/status
164187
```
165188

166-
Reports whether writes are encrypted, the master-key fingerprint, encrypted vs plaintext file
167-
counts, and every key row with its status history. All endpoints under
168-
`/api/v1/admin/storage-encryption` require an admin account.
189+
Reports whether writes are encrypted, the master-key fingerprint, where the key came from
190+
(`masterKeySource`: `config`, `environment` or `generated`), the storage backend, encrypted vs
191+
plaintext file counts, the number of key rows still on an older master key, and every key row with
192+
its status history. All endpoints under `/api/v1/admin/storage-encryption` require an admin
193+
account.
194+
195+
The same information is on the Storage tab under Infrastructure in the admin UI, which is the
196+
surface most operators should use.
169197

170198
## What this protects against
171199

frontend/editor/public/locales/en-US/translation.toml

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8344,6 +8344,172 @@ subtitleCreated = "Copy this secret now — it won't be shown again."
83448344
title = "Create API key"
83458345
titleCreated = "Key created"
83468346

8347+
[portal.infrastructure.encryption]
8348+
heading = "Encryption at rest"
8349+
hintLabel = "More about this section"
8350+
subheading = "Stored files are encrypted before they reach disk, the database or object storage."
8351+
unknown = "Unknown"
8352+
8353+
[portal.infrastructure.encryption.auditNotice]
8354+
description = "Encryption is active on this licence, but encrypt, decrypt, revocation and export events are not written to the audit trail. Encryption itself is unaffected."
8355+
title = "Audit events need an Enterprise licence"
8356+
8357+
[portal.infrastructure.encryption.coverage]
8358+
decryptOnlyNote = "New uploads are stored as plaintext. Files encrypted earlier are still readable."
8359+
encrypted = "Encrypted files"
8360+
heading = "Coverage"
8361+
percentEncrypted = "{{percent}}% encrypted"
8362+
plaintext = "Plaintext files"
8363+
progressLabel = "{{percent}}% of stored files encrypted"
8364+
subheading = "Covers files in My Files and signing workflows. Files a pipeline reads in place, including network sources, are not stored here and are not encrypted."
8365+
8366+
[portal.infrastructure.encryption.error]
8367+
action = "That key change could not be applied. Please try again."
8368+
keyMissing = "That key no longer exists. Refresh to see the current list."
8369+
migrationStart = "The migration could not be started. Please try again."
8370+
rotate = "The re-wrap could not be completed. Please try again."
8371+
8372+
[portal.infrastructure.encryption.generatedKey]
8373+
backupGuide = "How to back it up"
8374+
description = "No encryption key was configured, so one was created on this server. Back it up now: if it is lost, encrypted files cannot be recovered."
8375+
title = "This key was generated for you"
8376+
8377+
[portal.infrastructure.encryption.keys]
8378+
changedAt = "Changed {{when}}"
8379+
changedBy = "By {{actor}}"
8380+
changedByAt = "By {{actor}} on {{when}}"
8381+
heading = "Scope keys"
8382+
keyVersion = "Version {{version}}"
8383+
neverChanged = "Never changed"
8384+
restore = "Restore access"
8385+
retiredNote = "A retired key still decrypts the files stored under it, but a newer key wraps new uploads."
8386+
revoke = "Revoke"
8387+
subheading = "One key per scope. Revoking a key blocks reads of everything stored under it."
8388+
8389+
[portal.infrastructure.encryption.keys.columns]
8390+
actions = "Actions"
8391+
lastChange = "Last change"
8392+
scope = "Scope"
8393+
status = "Status"
8394+
8395+
[portal.infrastructure.encryption.keys.empty]
8396+
description = "A key is created the first time someone uploads a file, not at startup."
8397+
title = "No keys yet"
8398+
8399+
[portal.infrastructure.encryption.masterKey]
8400+
backend = "Storage backend"
8401+
compareNote = "Compare this against the fingerprint in your key backup and in the startup log."
8402+
copied = "Copied"
8403+
copy = "Copy"
8404+
copyFailed = "This browser would not allow copying. The fingerprint is selected, so copy it manually."
8405+
fingerprint = "Fingerprint"
8406+
fingerprintHelp = "What is this fingerprint for?"
8407+
heading = "Master key"
8408+
notMaterialised = "The key machinery has not started on this node yet."
8409+
s3StreamingNote = "While anything is encrypted, object-store downloads are streamed through this server rather than redirected, so they use its bandwidth."
8410+
subheading = "Wraps every scope key. Never shown or accepted over the API."
8411+
version = "Version"
8412+
8413+
[portal.infrastructure.encryption.migration]
8414+
backlog_one = "{{formatted}} file is stored as plaintext and can be encrypted now."
8415+
backlog_other = "{{formatted}} files are stored as plaintext and can be encrypted now."
8416+
encrypted = "Encrypted"
8417+
failedCount = "Failed"
8418+
heading = "Encrypt existing files"
8419+
hint = "Encrypts the plaintext backlog left from before encryption was switched on. Progress is held in memory, so a backend restart resets it to not started. Running again skips files that are already encrypted."
8420+
noBacklog = "Every stored file is already encrypted."
8421+
progressLabel = "{{processed}} of {{total}} files encrypted"
8422+
requiresEncryptionOn = "Switch encryption on before running this: the backlog is encrypted using the same write path as new uploads."
8423+
runAgain = "Run again"
8424+
skipped = "Skipped"
8425+
skippedHelp = "What does skipped mean?"
8426+
skippedNote = "Skipped files were replaced by a user while the run was in progress, so their copy was left alone."
8427+
start = "Start"
8428+
startedAt = "Started {{when}}"
8429+
8430+
[portal.infrastructure.encryption.migration.confirm]
8431+
cancel = "Cancel"
8432+
confirm = "Start encrypting"
8433+
noCancel = "There is no stop control. Once started, the run continues until it finishes or encryption is switched off."
8434+
rewrites_one = "Rewrites {{formatted}} stored file, replacing the plaintext copy with an encrypted one."
8435+
rewrites_other = "Rewrites {{formatted}} stored files, replacing the plaintext copies with encrypted ones."
8436+
throttled = "The job is throttled and safe to re-run. Files already encrypted are skipped."
8437+
title = "Encrypt the existing backlog?"
8438+
8439+
[portal.infrastructure.encryption.migration.failed]
8440+
description = "Encryption was switched off while the migration was running, so the remaining files were left untouched. Switch it back on and run again."
8441+
title = "The run stopped early"
8442+
8443+
[portal.infrastructure.encryption.migration.state]
8444+
COMPLETED = "Completed"
8445+
FAILED = "Failed"
8446+
IDLE = "Not started"
8447+
RUNNING = "Running"
8448+
8449+
[portal.infrastructure.encryption.revoke]
8450+
cancel = "Cancel"
8451+
clusterDelay = "On a cluster, other nodes apply this within 60 seconds."
8452+
confirm = "Revoke access"
8453+
readsFail = "Files already stored under this key stop opening, for everyone."
8454+
reversible = "This is reversible. No key material is deleted."
8455+
title = "Revoke access to this key?"
8456+
uploadsContinue = "New uploads keep working. They get a fresh key, so they stay readable."
8457+
8458+
[portal.infrastructure.encryption.rotation]
8459+
currentVersion = "Current version"
8460+
heading = "Master key rotation"
8461+
hint = "Re-wraps scope keys under a new master key; file contents are never rewritten. The new key is set in configuration and applied on restart, so this button only performs the re-wrap."
8462+
pendingRows = "Rows on an older version"
8463+
rewrap = "Re-wrap keys"
8464+
rewrapped_one = "{{count}} key row re-wrapped."
8465+
rewrapped_other = "{{count}} key rows re-wrapped."
8466+
runbook = "Rotation runbook"
8467+
8468+
[portal.infrastructure.encryption.rotation.confirm]
8469+
cancel = "Cancel"
8470+
confirm = "Re-wrap keys"
8471+
noFiles = "File contents are not touched. Only the key table is rewritten."
8472+
rerunnable = "Safe to run again if it does not finish."
8473+
rewraps_one = "Re-wraps {{count}} key row under the current master key."
8474+
rewraps_other = "Re-wraps {{count}} key rows under the current master key."
8475+
title = "Re-wrap the key table?"
8476+
8477+
[portal.infrastructure.encryption.rotation.pending]
8478+
description = "Do not remove the previous key from configuration yet. Files under those keys would stop opening."
8479+
title_one = "{{count}} key row is still on the previous master key"
8480+
title_other = "{{count}} key rows are still on the previous master key"
8481+
8482+
[portal.infrastructure.encryption.scope]
8483+
global = "All users"
8484+
source = "Source {{id}}"
8485+
team = "Team {{id}}"
8486+
8487+
[portal.infrastructure.encryption.status]
8488+
ACTIVE = "Active"
8489+
DISABLED = "Revoked"
8490+
RETIRED = "Retired"
8491+
8492+
[portal.infrastructure.encryption.unavailable.forbidden]
8493+
description = "Your account does not have permission to view encryption settings."
8494+
title = "Not available for your account"
8495+
8496+
[portal.infrastructure.encryption.unavailable.registry-unavailable]
8497+
description = "The encryption key registry could not be read. Check the database connection and the application logs."
8498+
title = "Key registry unavailable"
8499+
8500+
[portal.infrastructure.encryption.unavailable.storage-disabled]
8501+
description = "File storage is switched off on this server, so there is nothing to encrypt."
8502+
title = "Storage is disabled"
8503+
8504+
[portal.infrastructure.encryption.unavailable.unknown]
8505+
description = "Encryption status could not be loaded. Please try again."
8506+
title = "Status unavailable"
8507+
8508+
[portal.infrastructure.encryption.writeState]
8509+
decryptOnly = "Decrypt only"
8510+
encrypting = "Encrypting new uploads"
8511+
off = "Off"
8512+
83478513
[portal.infrastructure.keyLabel]
83488514
active = "Active"
83498515
revoked = "Revoked"

frontend/editor/src/core/i18n/translationAudit.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ export const I18N_PROJECTS: TranslationProject[] = [
112112
// A failure's disabled reason arrives from the server as a key and is rendered with
113113
// t(thatKey), so nothing in source names it, but the copy still has to exist.
114114
/^portal\.failures\.disabled\./,
115+
// Encryption panel copy keyed by backend enum values: key status, write
116+
// state, migration state, and the reason a status read was refused. Each
117+
// is t(`...${value}`) where the value comes from the API response.
118+
/^portal\.infrastructure\.encryption\.(status|writeState|unavailable)\./,
119+
/^portal\.infrastructure\.encryption\.migration\.state\./,
115120
],
116121
minUsedKeys: 100,
117122
minLocaleKeys: 100,

0 commit comments

Comments
 (0)