Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/extensions/extensions.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,20 @@ const baseExtensions: Extension[] = [
importName: 'pg_ivm',
size: 24865,
},
{
name: 'pgcrypto',
description: `
The pgcrypto module provides cryptographic functions for PostgreSQL.
`,
shortDescription:
'The pgcrypto module provides cryptographic functions for PostgreSQL.',
docs: 'https://www.postgresql.org/docs/current/pgcrypto.html',
tags: ['postgres extension', 'postgres/contrib'],
importPath: '@electric-sql/pglite/contrib/pgcrypto',
importName: 'pgcrypto',
core: true,
size: 1148162,
},
]

const tags = [
Expand Down
3 changes: 2 additions & 1 deletion docs/repl/allExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ export { pg_buffercache } from '@electric-sql/pglite/contrib/pg_buffercache'
export { pg_freespacemap } from '@electric-sql/pglite/contrib/pg_freespacemap'
export { pg_surgery } from '@electric-sql/pglite/contrib/pg_surgery'
export { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm'
export { pg_uuidv7 } from '@electric-sql/pglite/pg_uuidv7'
export { pg_visibility } from '@electric-sql/pglite/contrib/pg_visibility'
export { pg_walinspect } from '@electric-sql/pglite/contrib/pg_walinspect'
export { pgcrypto } from '@electric-sql/pglite/contrib/pgcrypto'
export { pgtap } from '@electric-sql/pglite/pgtap'
export { pg_uuidv7 } from '@electric-sql/pglite/pg_uuidv7'
export { seg } from '@electric-sql/pglite/contrib/seg'
export { tablefunc } from '@electric-sql/pglite/contrib/tablefunc'
export { tcn } from '@electric-sql/pglite/contrib/tcn'
Expand Down
20 changes: 20 additions & 0 deletions packages/pglite-socket/tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

await new Promise<void>((resolve) => {
serverProcess.on('exit', (code) => {
expect(code).toBe(0)
Expand Down Expand Up @@ -78,6 +82,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

// Wait for server to start
await waitForPort(testPort)

Expand Down Expand Up @@ -126,6 +134,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

// Wait for server to be ready
const isReady = await waitForPort(testPort)
expect(isReady).toBe(true)
Expand Down Expand Up @@ -159,6 +171,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

const isReady = await waitForPort(testPort)
expect(isReady).toBe(true)
expect(output).toContain('Initializing PGLite with database: memory://')
Expand Down Expand Up @@ -198,6 +214,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

const isReady = await waitForPort(testPort)
expect(isReady).toBe(true)
serverProcess.kill()
Expand Down
1 change: 1 addition & 0 deletions packages/pglite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"bun": "^1.1.30",
"concurrently": "^8.2.2",
"http-server": "^14.1.1",
"openpgp": "^6.3.0",
"playwright": "^1.48.0",
"tinytar": "^0.1.0",
"vitest": "^2.1.2"
Expand Down
16 changes: 16 additions & 0 deletions packages/pglite/src/contrib/pgcrypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {
Extension,
ExtensionSetupResult,
PGliteInterface,
} from '../interface'

const setup = async (_pg: PGliteInterface, _emscriptenOpts: any) => {
return {
bundlePath: new URL('../../release/pgcrypto.tar.gz', import.meta.url),
} satisfies ExtensionSetupResult
}

export const pgcrypto = {
name: 'pgcrypto',
setup,
} satisfies Extension
252 changes: 252 additions & 0 deletions packages/pglite/tests/contrib/pgcrypto.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import { describe, it, expect } from 'vitest'
import { PGlite } from '../../dist/index.js'
import { pgcrypto } from '../../dist/contrib/pgcrypto.js'
import * as openpgp from 'openpgp'

describe('pg_pgcryptotrgm', () => {
it('digest', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
"SELECT encode(digest(convert_to('test', 'UTF8'), 'sha1'), 'hex') as value;",
)
expect(res.rows[0].value, 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3')
})

it('hmac', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
"SELECT encode(hmac(convert_to('test', 'UTF8'), convert_to('key', 'UTF8'), 'sha1'), 'hex') as value;",
)
expect(res.rows[0].value).toEqual(
'671f54ce0c540f78ffe1e26dcf9c2a047aea4fda',
)
})

it('crypt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query("SELECT crypt('test', gen_salt('bf')) as value;")
expect(res.rows[0].value.length).toEqual(60)
})

it('gen_salt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query("SELECT gen_salt('bf') as value;")
expect(res.rows[0].value.length).toEqual(29)
})

it('armor', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query("SELECT armor(digest('test', 'sha1')) as value;")
expect(res.rows[0].value).toContain('-----BEGIN PGP MESSAGE-----')
expect(res.rows[0].value).toContain('-----END PGP MESSAGE-----')
})

it('pgp_sym_encrypt and pgp_sym_decrypt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
"SELECT pgp_sym_encrypt('test', 'key') as value;",
)
const encrypted = res.rows[0].value

const res2 = await pg.query("SELECT pgp_sym_decrypt($1, 'key') as value;", [
encrypted,
])
expect(res2.rows[0].value).toEqual('test')
})

it('pgp_pub_encrypt and pgp_pub_decrypt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const { privateKey, publicKey } = await openpgp.generateKey({
type: 'rsa',
rsaBits: 2048,
userIDs: [{ name: 'PGlite', email: 'hello@pglite.dev' }],
passphrase: '',
})

const toEncrypt = 'PGlite@$#%!^$&*WQFgjqPkVERewfreg094340f1012-='

const e2 = await pg.exec(
`
WITH encrypted AS (
SELECT pgp_pub_encrypt('${toEncrypt}', dearmor('${publicKey}')) AS encrypted
)
SELECT
pgp_pub_decrypt(encrypted, dearmor('${privateKey}')) as decrypted_output
FROM encrypted;
`,
)
expect(e2[0].rows[0].decrypted_output, toEncrypt)
})

it('pgp_key_id', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const { publicKey } = await openpgp.generateKey({
type: 'rsa',
rsaBits: 2048,
userIDs: [{ name: 'PGlite', email: 'hello@pglite.dev' }],
passphrase: '',
})

const res = await pg.query(
`SELECT pgp_key_id(dearmor('${publicKey}')) as value;`,
)
// pgp_key_id returns a 16-character hex string
expect(res.rows[0].value).toHaveLength(16)
expect(res.rows[0].value).toMatch(/^[0-9A-F]+$/)
})

it('pgp_armor_headers', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

// Create armored data with headers
const res = await pg.query(
`SELECT armor(digest('test', 'sha1'), ARRAY['key1'], ARRAY['value1']) as armored;`,
)
const armored = res.rows[0].armored

const res2 = await pg.query(`SELECT * FROM pgp_armor_headers($1);`, [
armored,
])
expect(res2.rows).toContainEqual({ key: 'key1', value: 'value1' })
})

it('encrypt and decrypt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
`SELECT encrypt('test data'::bytea, 'secret key'::bytea, 'aes') as encrypted;`,
)
const encrypted = res.rows[0].encrypted

const res2 = await pg.query(
`SELECT convert_from(decrypt($1, 'secret key'::bytea, 'aes'), 'UTF8') as decrypted;`,
[encrypted],
)
expect(res2.rows[0].decrypted).toEqual('test data')
})

it('encrypt_iv and decrypt_iv', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

// AES block size is 16 bytes, so IV must be 16 bytes
const iv = '1234567890123456'

const res = await pg.query(
`SELECT encrypt_iv('test data'::bytea, 'secret key'::bytea, '${iv}'::bytea, 'aes') as encrypted;`,
)
const encrypted = res.rows[0].encrypted

const res2 = await pg.query(
`SELECT convert_from(decrypt_iv($1, 'secret key'::bytea, '${iv}'::bytea, 'aes'), 'UTF8') as decrypted;`,
[encrypted],
)
expect(res2.rows[0].decrypted).toEqual('test data')
})

it('gen_random_bytes', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
`SELECT length(gen_random_bytes(32)) as len, encode(gen_random_bytes(16), 'hex') as bytes;`,
)
expect(res.rows[0].len).toEqual(32)
// 16 bytes = 32 hex characters
expect(res.rows[0].bytes).toHaveLength(32)
})

it('gen_random_uuid', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(`SELECT gen_random_uuid() as uuid;`)
// UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
expect(res.rows[0].uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
)
})
})
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.