@pgpm/encrypted-secrets-table 0.15.3 → 0.15.4
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.
- package/Makefile +1 -1
- package/package.json +4 -4
- package/pgpm-encrypted-secrets-table.control +1 -1
- package/__tests__/__snapshots__/secrets-table.test.ts.snap +0 -38
- package/__tests__/secrets-table.test.ts +0 -272
- /package/sql/{pgpm-encrypted-secrets-table--0.15.2.sql → pgpm-encrypted-secrets-table--0.15.3.sql} +0 -0
package/Makefile
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpm/encrypted-secrets-table",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.4",
|
|
4
4
|
"description": "Table-based encrypted secrets storage and retrieval",
|
|
5
5
|
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
6
6
|
"contributors": [
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"test:watch": "jest --watch"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@pgpm/verify": "0.15.
|
|
24
|
+
"@pgpm/verify": "0.15.4"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"pgpm": "^1.
|
|
27
|
+
"pgpm": "^1.2.2"
|
|
28
28
|
},
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
"bugs": {
|
|
35
35
|
"url": "https://github.com/constructive-io/pgpm-modules/issues"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "aad0dbef0336d6c18d027120ef9addc418822edd"
|
|
38
38
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pgpm-encrypted-secrets-table extension
|
|
2
2
|
comment = 'pgpm-encrypted-secrets-table extension'
|
|
3
|
-
default_version = '0.15.
|
|
3
|
+
default_version = '0.15.3'
|
|
4
4
|
module_pathname = '$libdir/pgpm-encrypted-secrets-table'
|
|
5
5
|
requires = 'pgcrypto,plpgsql,uuid-ossp,pgpm-verify'
|
|
6
6
|
relocatable = false
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
-
|
|
3
|
-
exports[`encrypted secrets table should have secrets_table with correct structure 1`] = `
|
|
4
|
-
{
|
|
5
|
-
"columns": [
|
|
6
|
-
{
|
|
7
|
-
"column_default": "uuid_generate_v4()",
|
|
8
|
-
"column_name": "id",
|
|
9
|
-
"data_type": "uuid",
|
|
10
|
-
"is_nullable": "NO",
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"column_default": null,
|
|
14
|
-
"column_name": "secrets_owned_field",
|
|
15
|
-
"data_type": "uuid",
|
|
16
|
-
"is_nullable": "NO",
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"column_default": null,
|
|
20
|
-
"column_name": "name",
|
|
21
|
-
"data_type": "text",
|
|
22
|
-
"is_nullable": "NO",
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"column_default": null,
|
|
26
|
-
"column_name": "secrets_value_field",
|
|
27
|
-
"data_type": "bytea",
|
|
28
|
-
"is_nullable": "YES",
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"column_default": null,
|
|
32
|
-
"column_name": "secrets_enc_field",
|
|
33
|
-
"data_type": "text",
|
|
34
|
-
"is_nullable": "YES",
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
}
|
|
38
|
-
`;
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { getConnections, PgTestClient, snapshot } from 'pgsql-test';
|
|
2
|
-
|
|
3
|
-
let pg: PgTestClient;
|
|
4
|
-
let teardown: () => Promise<void>;
|
|
5
|
-
|
|
6
|
-
const user_id = 'dc474833-318a-41f5-9239-ee563ab657a6';
|
|
7
|
-
const user_id_2 = '550e8400-e29b-41d4-a716-446655440000';
|
|
8
|
-
|
|
9
|
-
describe('encrypted secrets table', () => {
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
({ pg, teardown } = await getConnections());
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterAll(async () => {
|
|
15
|
-
await teardown();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
await pg.beforeEach();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
await pg.afterEach();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should have secrets_schema created', async () => {
|
|
27
|
-
const schemas = await pg.any(
|
|
28
|
-
`SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'secrets_schema'`
|
|
29
|
-
);
|
|
30
|
-
expect(schemas).toHaveLength(1);
|
|
31
|
-
expect(schemas[0].schema_name).toBe('secrets_schema');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should have secrets_table with correct structure', async () => {
|
|
35
|
-
const columns = await pg.any(
|
|
36
|
-
`SELECT column_name, data_type, is_nullable, column_default
|
|
37
|
-
FROM information_schema.columns
|
|
38
|
-
WHERE table_schema = 'secrets_schema' AND table_name = 'secrets_table'
|
|
39
|
-
ORDER BY ordinal_position`
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
expect(snapshot({ columns })).toMatchSnapshot();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should have unique constraint on (secrets_owned_field, name)', async () => {
|
|
46
|
-
const constraints = await pg.any(
|
|
47
|
-
`SELECT constraint_name, constraint_type
|
|
48
|
-
FROM information_schema.table_constraints
|
|
49
|
-
WHERE table_schema = 'secrets_schema'
|
|
50
|
-
AND table_name = 'secrets_table'
|
|
51
|
-
AND constraint_type = 'UNIQUE'`
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
expect(constraints).toHaveLength(1);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should insert record with default values', async () => {
|
|
58
|
-
const [result] = await pg.any(
|
|
59
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
60
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
61
|
-
VALUES ($1::uuid, 'test-secret', 'test-value'::bytea, 'none')
|
|
62
|
-
RETURNING *`,
|
|
63
|
-
[user_id]
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
expect(result.secrets_owned_field).toBe(user_id);
|
|
67
|
-
expect(result.name).toBe('test-secret');
|
|
68
|
-
expect(result.secrets_enc_field).toBe('none');
|
|
69
|
-
expect(result.id).toBeDefined();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should enforce unique constraint on (secrets_owned_field, name)', async () => {
|
|
73
|
-
// Insert first record
|
|
74
|
-
await pg.any(
|
|
75
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
76
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
77
|
-
VALUES ($1::uuid, 'duplicate-name', 'value1'::bytea, 'none')`,
|
|
78
|
-
[user_id]
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
// Try to insert duplicate - should fail
|
|
82
|
-
await expect(
|
|
83
|
-
pg.any(
|
|
84
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
85
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
86
|
-
VALUES ($1::uuid, 'duplicate-name', 'value2'::bytea, 'none')`,
|
|
87
|
-
[user_id]
|
|
88
|
-
)
|
|
89
|
-
).rejects.toThrow();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should allow same name for different users', async () => {
|
|
93
|
-
// Insert for first user
|
|
94
|
-
await pg.any(
|
|
95
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
96
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
97
|
-
VALUES ($1::uuid, 'same-name', 'value1'::bytea, 'none')`,
|
|
98
|
-
[user_id]
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Insert for second user - should succeed
|
|
102
|
-
const [result] = await pg.any(
|
|
103
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
104
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
105
|
-
VALUES ($1::uuid, 'same-name', 'value2'::bytea, 'none')
|
|
106
|
-
RETURNING *`,
|
|
107
|
-
[user_id_2]
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
expect(result.secrets_owned_field).toBe(user_id_2);
|
|
111
|
-
expect(result.name).toBe('same-name');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should trigger hash_secrets on insert with crypt', async () => {
|
|
115
|
-
const plaintext = 'my-secret-password';
|
|
116
|
-
|
|
117
|
-
const [result] = await pg.any(
|
|
118
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
119
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
120
|
-
VALUES ($1::uuid, 'crypt-secret', $2::bytea, 'crypt')
|
|
121
|
-
RETURNING *`,
|
|
122
|
-
[user_id, plaintext]
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
// The trigger should have hashed the value
|
|
126
|
-
expect(result.secrets_enc_field).toBe('crypt');
|
|
127
|
-
expect(result.secrets_value_field).not.toEqual(Buffer.from(plaintext));
|
|
128
|
-
|
|
129
|
-
// Verify it's a bcrypt hash (starts with $2)
|
|
130
|
-
const hashedValue = result.secrets_value_field.toString();
|
|
131
|
-
expect(hashedValue).toMatch(/^\$2[aby]?\$/);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should trigger hash_secrets on insert with pgp', async () => {
|
|
135
|
-
const plaintext = 'my-secret-data';
|
|
136
|
-
|
|
137
|
-
const [result] = await pg.any(
|
|
138
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
139
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
140
|
-
VALUES ($1::uuid, 'pgp-secret', $2::bytea, 'pgp')
|
|
141
|
-
RETURNING *`,
|
|
142
|
-
[user_id, plaintext]
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// The trigger should have encrypted the value
|
|
146
|
-
expect(result.secrets_enc_field).toBe('pgp');
|
|
147
|
-
expect(result.secrets_value_field).not.toEqual(Buffer.from(plaintext));
|
|
148
|
-
|
|
149
|
-
// Should be longer than original due to encryption
|
|
150
|
-
expect(result.secrets_value_field.length).toBeGreaterThan(plaintext.length);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should default to none encryption when not specified', async () => {
|
|
154
|
-
const plaintext = 'unencrypted-data';
|
|
155
|
-
|
|
156
|
-
const [result] = await pg.any(
|
|
157
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
158
|
-
(secrets_owned_field, name, secrets_value_field)
|
|
159
|
-
VALUES ($1::uuid, 'none-secret', $2::bytea)
|
|
160
|
-
RETURNING *`,
|
|
161
|
-
[user_id, plaintext]
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
// The trigger should set enc_field to 'none'
|
|
165
|
-
expect(result.secrets_enc_field).toBe('none');
|
|
166
|
-
expect(result.secrets_value_field).toEqual(Buffer.from(plaintext));
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should trigger hash_secrets on update when value changes', async () => {
|
|
170
|
-
// Insert initial record
|
|
171
|
-
const [initial] = await pg.any(
|
|
172
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
173
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
174
|
-
VALUES ($1::uuid, 'update-test', 'initial-value'::bytea, 'none')
|
|
175
|
-
RETURNING *`,
|
|
176
|
-
[user_id]
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
// Update to use crypt encryption
|
|
180
|
-
const [updated] = await pg.any(
|
|
181
|
-
`UPDATE secrets_schema.secrets_table
|
|
182
|
-
SET secrets_value_field = 'new-password'::bytea, secrets_enc_field = 'crypt'
|
|
183
|
-
WHERE id = $1
|
|
184
|
-
RETURNING *`,
|
|
185
|
-
[initial.id]
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// Should be encrypted now
|
|
189
|
-
expect(updated.secrets_enc_field).toBe('crypt');
|
|
190
|
-
expect(updated.secrets_value_field).not.toEqual(Buffer.from('new-password'));
|
|
191
|
-
|
|
192
|
-
// Verify it's a bcrypt hash
|
|
193
|
-
const hashedValue = updated.secrets_value_field.toString();
|
|
194
|
-
expect(hashedValue).toMatch(/^\$2[aby]?\$/);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('should not trigger hash_secrets on update when value unchanged', async () => {
|
|
198
|
-
// Insert initial record with crypt
|
|
199
|
-
const [initial] = await pg.any(
|
|
200
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
201
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
202
|
-
VALUES ($1::uuid, 'no-update-test', 'password'::bytea, 'crypt')
|
|
203
|
-
RETURNING *`,
|
|
204
|
-
[user_id]
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
const originalHash = initial.secrets_value_field;
|
|
208
|
-
|
|
209
|
-
// Update name only (not the value field)
|
|
210
|
-
const [updated] = await pg.any(
|
|
211
|
-
`UPDATE secrets_schema.secrets_table
|
|
212
|
-
SET name = 'renamed-secret'
|
|
213
|
-
WHERE id = $1
|
|
214
|
-
RETURNING *`,
|
|
215
|
-
[initial.id]
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// Hash should remain the same
|
|
219
|
-
expect(updated.secrets_value_field).toEqual(originalHash);
|
|
220
|
-
expect(updated.name).toBe('renamed-secret');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('should verify crypt hash works correctly', async () => {
|
|
224
|
-
const password = 'test-password-123';
|
|
225
|
-
|
|
226
|
-
// Insert with crypt
|
|
227
|
-
const [result] = await pg.any(
|
|
228
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
229
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
230
|
-
VALUES ($1::uuid, 'crypt-verify', $2::bytea, 'crypt')
|
|
231
|
-
RETURNING *`,
|
|
232
|
-
[user_id, password]
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
// The stored hash should be different from the original password
|
|
236
|
-
const storedHash = result.secrets_value_field.toString();
|
|
237
|
-
expect(storedHash).not.toBe(password);
|
|
238
|
-
|
|
239
|
-
// Verify it's a proper bcrypt hash format
|
|
240
|
-
expect(storedHash).toMatch(/^\$2[aby]?\$/);
|
|
241
|
-
|
|
242
|
-
// Verify the hash is the expected length (bcrypt hashes are typically 60 characters)
|
|
243
|
-
expect(storedHash.length).toBe(60);
|
|
244
|
-
|
|
245
|
-
// Verify that inserting the same password again produces a different hash (salt should be different)
|
|
246
|
-
const [result2] = await pg.any(
|
|
247
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
248
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
249
|
-
VALUES ($1::uuid, 'crypt-verify-2', $2::bytea, 'crypt')
|
|
250
|
-
RETURNING *`,
|
|
251
|
-
[user_id, password]
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
const storedHash2 = result2.secrets_value_field.toString();
|
|
255
|
-
expect(storedHash2).not.toBe(storedHash); // Different salt = different hash
|
|
256
|
-
expect(storedHash2).toMatch(/^\$2[aby]?\$/);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('should handle null values correctly', async () => {
|
|
260
|
-
const [result] = await pg.any(
|
|
261
|
-
`INSERT INTO secrets_schema.secrets_table
|
|
262
|
-
(secrets_owned_field, name, secrets_value_field, secrets_enc_field)
|
|
263
|
-
VALUES ($1::uuid, 'null-test', NULL, NULL)
|
|
264
|
-
RETURNING *`,
|
|
265
|
-
[user_id]
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
// Trigger should set enc_field to 'none' even when value is null
|
|
269
|
-
expect(result.secrets_enc_field).toBe('none');
|
|
270
|
-
expect(result.secrets_value_field).toBeNull();
|
|
271
|
-
});
|
|
272
|
-
});
|
/package/sql/{pgpm-encrypted-secrets-table--0.15.2.sql → pgpm-encrypted-secrets-table--0.15.3.sql}
RENAMED
|
File without changes
|