Skip to content
This repository was archived by the owner on Jul 15, 2025. It is now read-only.

Commit ed3af49

Browse files
danny-avilaopenbiocure
authored andcommitted
🔑 feat: Base64 Google Service Keys and Reliable Private Key Formats (danny-avila#8385)
1 parent f118475 commit ed3af49

File tree

2 files changed

+126
-4
lines changed

2 files changed

+126
-4
lines changed

‎packages/api/src/utils/key.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,89 @@ describe('loadServiceKey', () => {
9494
const result = await loadServiceKey(JSON.stringify(invalidServiceKey));
9595
expect(result).toEqual(invalidServiceKey); // It returns the object as-is, validation is minimal
9696
});
97+
98+
it('should handle escaped newlines in private key from AWS Secrets Manager', async () => {
99+
const serviceKeyWithEscapedNewlines = {
100+
...mockServiceKey,
101+
private_key: '-----BEGIN PRIVATE KEY-----\\ntest-key\\n-----END PRIVATE KEY-----',
102+
};
103+
const jsonString = JSON.stringify(serviceKeyWithEscapedNewlines);
104+
105+
const result = await loadServiceKey(jsonString);
106+
expect(result).not.toBeNull();
107+
expect(result?.private_key).toBe(
108+
'-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----',
109+
);
110+
});
111+
112+
it('should handle double-escaped newlines in private key', async () => {
113+
// When you have \\n in JavaScript, JSON.stringify converts it to \\\\n
114+
// But we want to test the case where the JSON string contains \\n (single backslash + n)
115+
const serviceKeyWithEscapedNewlines = {
116+
...mockServiceKey,
117+
private_key: '-----BEGIN PRIVATE KEY-----\\ntest-key\\n-----END PRIVATE KEY-----',
118+
};
119+
// This will create a JSON string where the private_key contains literal \n (backslash-n)
120+
const jsonString = JSON.stringify(serviceKeyWithEscapedNewlines);
121+
122+
const result = await loadServiceKey(jsonString);
123+
expect(result).not.toBeNull();
124+
expect(result?.private_key).toBe(
125+
'-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----',
126+
);
127+
});
128+
129+
it('should handle private key without any newlines', async () => {
130+
const serviceKeyWithoutNewlines = {
131+
...mockServiceKey,
132+
private_key: '-----BEGIN PRIVATE KEY-----test-key-----END PRIVATE KEY-----',
133+
};
134+
const jsonString = JSON.stringify(serviceKeyWithoutNewlines);
135+
136+
const result = await loadServiceKey(jsonString);
137+
expect(result).not.toBeNull();
138+
expect(result?.private_key).toBe(
139+
'-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----',
140+
);
141+
});
142+
143+
it('should not modify private key that already has proper formatting', async () => {
144+
const jsonString = JSON.stringify(mockServiceKey);
145+
146+
const result = await loadServiceKey(jsonString);
147+
expect(result).not.toBeNull();
148+
expect(result?.private_key).toBe(mockServiceKey.private_key);
149+
});
150+
151+
it('should handle base64 encoded service key', async () => {
152+
const jsonString = JSON.stringify(mockServiceKey);
153+
const base64Encoded = Buffer.from(jsonString).toString('base64');
154+
155+
const result = await loadServiceKey(base64Encoded);
156+
expect(result).not.toBeNull();
157+
expect(result).toEqual(mockServiceKey);
158+
});
159+
160+
it('should handle base64 encoded service key with escaped newlines', async () => {
161+
const serviceKeyWithEscapedNewlines = {
162+
...mockServiceKey,
163+
private_key: '-----BEGIN PRIVATE KEY-----\\ntest-key\\n-----END PRIVATE KEY-----',
164+
};
165+
const jsonString = JSON.stringify(serviceKeyWithEscapedNewlines);
166+
const base64Encoded = Buffer.from(jsonString).toString('base64');
167+
168+
const result = await loadServiceKey(base64Encoded);
169+
expect(result).not.toBeNull();
170+
expect(result?.private_key).toBe(
171+
'-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----',
172+
);
173+
});
174+
175+
it('should handle invalid base64 strings gracefully', async () => {
176+
// This looks like base64 but isn't valid
177+
const invalidBase64 = 'SGVsbG8gV29ybGQ='; // "Hello World" in base64, not valid JSON
178+
179+
const result = await loadServiceKey(invalidBase64);
180+
expect(result).toBeNull();
181+
});
97182
});

‎packages/api/src/utils/key.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,20 @@ export async function loadServiceKey(keyPath: string): Promise<GoogleServiceKey
2929

3030
let serviceKey: unknown;
3131

32+
// Check if it's base64 encoded (common pattern for storing in env vars)
33+
if (keyPath.trim().match(/^[A-Za-z0-9+/]+=*$/)) {
34+
try {
35+
const decoded = Buffer.from(keyPath.trim(), 'base64').toString('utf-8');
36+
// Try to parse the decoded string as JSON
37+
serviceKey = JSON.parse(decoded);
38+
} catch {
39+
// Not base64 or not valid JSON after decoding, continue with other methods
40+
// Silent failure - not critical
41+
}
42+
}
43+
3244
// Check if it's a stringified JSON (starts with '{')
33-
if (keyPath.trim().startsWith('{')) {
45+
if (!serviceKey && keyPath.trim().startsWith('{')) {
3446
try {
3547
serviceKey = JSON.parse(keyPath);
3648
} catch (error) {
@@ -39,15 +51,15 @@ export async function loadServiceKey(keyPath: string): Promise<GoogleServiceKey
3951
}
4052
}
4153
// Check if it's a URL
42-
else if (/^https?:\/\//.test(keyPath)) {
54+
else if (!serviceKey && /^https?:\/\//.test(keyPath)) {
4355
try {
4456
const response = await axios.get(keyPath);
4557
serviceKey = response.data;
4658
} catch (error) {
4759
logger.error(`Failed to fetch the service key from URL: ${keyPath}`, error);
4860
return null;
4961
}
50-
} else {
62+
} else if (!serviceKey) {
5163
// It's a file path
5264
try {
5365
const absolutePath = path.isAbsolute(keyPath) ? keyPath : path.resolve(keyPath);
@@ -75,5 +87,30 @@ export async function loadServiceKey(keyPath: string): Promise<GoogleServiceKey
7587
return null;
7688
}
7789

78-
return serviceKey as GoogleServiceKey;
90+
// Fix private key formatting if needed
91+
const key = serviceKey as GoogleServiceKey;
92+
if (key.private_key && typeof key.private_key === 'string') {
93+
// Replace escaped newlines with actual newlines
94+
// When JSON.parse processes "\\n", it becomes "\n" (single backslash + n)
95+
// When JSON.parse processes "\n", it becomes an actual newline character
96+
key.private_key = key.private_key.replace(/\\n/g, '\n');
97+
98+
// Also handle the String.raw`\n` case mentioned in Stack Overflow
99+
key.private_key = key.private_key.split(String.raw`\n`).join('\n');
100+
101+
// Ensure proper PEM format
102+
if (!key.private_key.includes('\n')) {
103+
// If no newlines are present, try to format it properly
104+
const privateKeyMatch = key.private_key.match(
105+
/^(-----BEGIN [A-Z ]+-----)(.*)(-----END [A-Z ]+-----)$/,
106+
);
107+
if (privateKeyMatch) {
108+
const [, header, body, footer] = privateKeyMatch;
109+
// Add newlines after header and before footer
110+
key.private_key = `${header}\n${body}\n${footer}`;
111+
}
112+
}
113+
}
114+
115+
return key;
79116
}

0 commit comments

Comments
 (0)