@ultimat3/core 22.14.0 → 23.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,6 +84,27 @@ export class SecretsKeyInvalidError extends UltimateError {
84
84
  }
85
85
  }
86
86
 
87
+ /**
88
+ * The same condition under the same code, for a key that is NOT the current one: a malformed entry
89
+ * of a ring variable (`ULTIMATE_SECRETS_RETIRED_KEYS`). Its own class because its repair is an
90
+ * edit of that variable — the current key is fine, and re-exporting it changes nothing — and
91
+ * because a class has one literal `fix:`, which is what `x errors explain` prints without an
92
+ * instance. A variable name that is not one never reaches the line.
93
+ */
94
+ export class SecretsRingKeyInvalidError extends UltimateError {
95
+ constructor(input: { at: string; found: number; expected: number; variable: string }) {
96
+ const variable = ENV_VAR_NAME.test(input.variable)
97
+ ? input.variable
98
+ : 'the variable the cause names';
99
+ super({
100
+ code: 'X_SECRETS_KEY_INVALID',
101
+ cause: `the master key in ${input.at} is ${input.found} character(s); an AES-256 key is ${input.expected} lowercase hex characters`,
102
+ fix: `x secrets edit # ${variable} holds ${input.expected}-character lowercase hex keys separated by commas: correct or remove the entry the cause names`,
103
+ meta: { at: input.at },
104
+ });
105
+ }
106
+ }
107
+
87
108
  /**
88
109
  * A well-formed key that is not the one this file was sealed with. Distinguishable from tampering
89
110
  * only because the envelope carries a key id — a domain-separated SHA-256 of the key, which is
package/src/secrets.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  SecretsKeyInvalidError,
9
9
  SecretsKeyMismatchError,
10
10
  SecretsPlaintextInvalidError,
11
+ SecretsRingKeyInvalidError,
11
12
  SecretsTamperedError,
12
13
  } from './secrets-errors';
13
14
 
@@ -65,14 +66,15 @@ function decodeHex(hex: string): Uint8Array<ArrayBuffer> {
65
66
  }
66
67
 
67
68
  // `btoa`/`atob` rather than node:buffer — both are standard globals, and a chunk loop avoids the
68
- // stack blow-up `String.fromCharCode(...bytes)` hits on a spread of any size.
69
- function encodeBase64(bytes: Uint8Array<ArrayBuffer>): string {
69
+ // stack blow-up `String.fromCharCode(...bytes)` hits on a spread of any size. Exported for
70
+ // `seal.ts`, which writes the same bytes in the URL-safe alphabet — one codec, never a second.
71
+ export function encodeBase64(bytes: Uint8Array<ArrayBuffer>): string {
70
72
  let binary = '';
71
73
  for (const byte of bytes) binary += String.fromCharCode(byte);
72
74
  return btoa(binary);
73
75
  }
74
76
 
75
- function decodeBase64(text: string): Uint8Array<ArrayBuffer> {
77
+ export function decodeBase64(text: string): Uint8Array<ArrayBuffer> {
76
78
  const binary = atob(text);
77
79
  const bytes = new Uint8Array(binary.length);
78
80
  for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
@@ -84,15 +86,22 @@ export function generateMasterKey(): string {
84
86
  return encodeHex(crypto.getRandomValues(new Uint8Array(SECRETS_KEY_BYTES)));
85
87
  }
86
88
 
87
- /** 64 lowercase hex characters, or `X_SECRETS_KEY_INVALID`. Whitespace is trimmed, never repaired. */
88
- export function parseMasterKey(raw: string, at: string): Uint8Array<ArrayBuffer> {
89
+ /**
90
+ * 64 lowercase hex characters, or `X_SECRETS_KEY_INVALID`. Whitespace is trimmed, never repaired.
91
+ * `variable` names the variable a key OTHER than the current one was read from, so the refusal's
92
+ * fix edits that variable instead of re-exporting a current key that is fine.
93
+ */
94
+ export function parseMasterKey(
95
+ raw: string,
96
+ at: string,
97
+ variable?: string,
98
+ ): Uint8Array<ArrayBuffer> {
89
99
  const hex = raw.trim();
90
100
  if (hex.length !== SECRETS_KEY_HEX_LENGTH || !HEX_KEY.test(hex)) {
91
- throw new SecretsKeyInvalidError({
92
- at,
93
- found: hex.length,
94
- expected: SECRETS_KEY_HEX_LENGTH,
95
- });
101
+ const shape = { at, found: hex.length, expected: SECRETS_KEY_HEX_LENGTH };
102
+ throw variable === undefined
103
+ ? new SecretsKeyInvalidError(shape)
104
+ : new SecretsRingKeyInvalidError({ ...shape, variable });
96
105
  }
97
106
  return decodeHex(hex);
98
107
  }
@@ -119,7 +128,8 @@ export async function masterKeyId(key: Uint8Array<ArrayBuffer>): Promise<string>
119
128
  const additionalData = (header: Omit<SecretsEnvelope, 'iv' | 'ct'>): Uint8Array<ArrayBuffer> =>
120
129
  encoder.encode(`ultimate.secrets|v=${header.v}|alg=${header.alg}|kid=${header.kid}`);
121
130
 
122
- const importKey = (key: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
131
+ /** The AES-256-GCM key object, non-extractable. Shared with `seal-keys.ts`: one import, one usage set. */
132
+ export const importKey = (key: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
123
133
  crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
124
134
 
125
135
  /**