├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── nix ├── pgScript.nix ├── pgsodium.nix ├── pgtap.nix ├── postgresql │ ├── 17.nix │ ├── default.nix │ ├── generic.nix │ └── patches │ │ ├── less-is-more.patch │ │ ├── locale-binary-path.patch │ │ ├── paths-for-split-outputs.patch │ │ ├── paths-with-postgresql-suffix.patch │ │ ├── relative-to-symlinks-16+.patch │ │ ├── relative-to-symlinks.patch │ │ ├── socketdir-in-run-13+.patch │ │ ├── socketdir-in-run.patch │ │ └── specify_pkglibdir_at_runtime.patch ├── supabase_vault.nix └── withTmpDb.sh.in ├── shell.nix ├── sql ├── supabase_vault--0.2.8--0.3.0.sql ├── supabase_vault--0.2.8.sql ├── supabase_vault--0.3.0--0.3.1.sql └── supabase_vault--0.3.0.sql ├── src ├── crypto_aead_det_xchacha20.c ├── crypto_aead_det_xchacha20.h ├── pgsodium.c ├── pgsodium.h └── supabase_vault.c ├── supabase_vault--0.2.8.control ├── supabase_vault.control.in └── test ├── expected └── test.out ├── fixtures.sql └── sql └── test.sql /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | pg-version: ['13', '14', '15', '16', '17'] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: cachix/install-nix-action@v13 19 | with: 20 | nix_path: nixpkgs=channel:nixos-unstable 21 | - name: Run tests 22 | run: nix-shell --run "vault-with-pg-${{ matrix.pg-version }} make installcheck" 23 | - if: ${{ failure() }} 24 | run: cat regression.diffs 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | results/ 2 | regression.* 3 | *.o 4 | *.so 5 | *.bc 6 | *.dylib 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Supabase Vault License 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | Copyright 2021 Supabase 181 | 182 | Licensed under the Apache License, Version 2.0 (the "License"); 183 | you may not use this file except in compliance with the License. 184 | You may obtain a copy of the License at 185 | 186 | http://www.apache.org/licenses/LICENSE-2.0 187 | 188 | Unless required by applicable law or agreed to in writing, software 189 | distributed under the License is distributed on an "AS IS" BASIS, 190 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 191 | See the License for the specific language governing permissions and 192 | limitations under the License. 193 | 194 | # pgsodium License 195 | 196 | Copyright (c) 2020 Michel Pelletier and Contributors 197 | 198 | Permission to use, copy, modify, and distribute this software and its 199 | documentation for any purpose, without fee, and without a written agreement 200 | is hereby granted, provided that the above copyright notice and this 201 | paragraph and the following two paragraphs appear in all copies. 202 | 203 | IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR 204 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING 205 | LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 206 | DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE 207 | POSSIBILITY OF SUCH DAMAGE. 208 | 209 | THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 210 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 211 | AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS 212 | ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO 213 | PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 214 | 215 | # libsodium-xchacha20-siv License 216 | 217 | BSD 2-Clause License 218 | 219 | Copyright (c) 2020-2024, Frank Denis 220 | All rights reserved. 221 | 222 | Redistribution and use in source and binary forms, with or without 223 | modification, are permitted provided that the following conditions are met: 224 | 225 | * Redistributions of source code must retain the above copyright notice, this 226 | list of conditions and the following disclaimer. 227 | 228 | * Redistributions in binary form must reproduce the above copyright notice, 229 | this list of conditions and the following disclaimer in the documentation 230 | and/or other materials provided with the distribution. 231 | 232 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 233 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 234 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 235 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 236 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 237 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 238 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 239 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 240 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 241 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 242 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PG_CFLAGS = -std=c99 -Werror -Wno-declaration-after-statement 2 | EXTENSION = supabase_vault 3 | EXTVERSION = 0.3.1 4 | 5 | DATA = $(wildcard sql/*--*.sql) 6 | 7 | TESTS = $(wildcard test/sql/*.sql) 8 | REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) 9 | REGRESS_OPTS = --use-existing --inputdir=test 10 | 11 | MODULE_big = $(EXTENSION) 12 | OBJS = $(patsubst %.c,%.o,$(wildcard src/*.c)) 13 | 14 | all: $(EXTENSION).control 15 | 16 | $(EXTENSION).control: 17 | sed "s/@VAULT_VERSION@/$(EXTVERSION)/g" $(EXTENSION).control.in > $(EXTENSION).control 18 | 19 | PG_CONFIG = pg_config 20 | SHLIB_LINK = -lsodium 21 | 22 | PG_CPPFLAGS := $(CPPFLAGS) -DEXTVERSION=\"$(EXTVERSION)\" 23 | 24 | PGXS := $(shell $(PG_CONFIG) --pgxs) 25 | include $(PGXS) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction to the Vault (Beta) 2 | 3 | Many applications have sensitive data that must have additional 4 | storage protection relative to other data. For example, your 5 | application may access external services with an "API Key". This key 6 | is issued to you by that specific external service provider, and you 7 | must keep it safe from being stolen or leaked. If someone got their 8 | hands on your payment processor key, for example, they may be able to 9 | use it to send money or digital assets out of your account to someone 10 | else. Wherever this key is stored, it would make sense to store it in 11 | an encrypted form. 12 | 13 | Supabase provides a table called `vault.secrets` that can be used to 14 | store sensitive information like API keys. These secrets will be 15 | stored in an encrypted format on disk and in any database dumps. This 16 | is often called [Encryption At 17 | Rest](https://en.wikipedia.org/wiki/Data_at_rest). Decrypting this 18 | table is done through a special database view called 19 | `vault.decrypted_secrets` that uses an encryption key that is itself 20 | not avaiable to SQL, but can be referred to by ID. Supabase manages 21 | these internal keys for you, so you can't leak them out of the 22 | database, you can only refer to them by their ids. 23 | 24 | ## Installation 25 | 26 | The Vault extension is enabled by default. If you install the Vault 27 | yourself locally, from SQL you can do: 28 | 29 | ``` 30 | CREATE EXTENSION supabase_vault CASCADE; 31 | ``` 32 | 33 | ## Using the Vault 34 | 35 | Using the vault is as simple as `INSERT`ing data into the 36 | `vault.secret` table. 37 | 38 | ``` 39 | postgres=> INSERT INTO vault.secrets (secret) VALUES ('s3kre3t_k3y') RETURNING *; 40 | -[ RECORD 1 ]------------------------------------------------------------- 41 | id | d91596b8-1047-446c-b9c0-66d98af6d001 42 | name | 43 | description | 44 | secret | S02eXS9BBY+kE3r621IS8beAytEEtj+dDHjs9/0AoMy7HTbog+ylxcS22A== 45 | key_id | 7f5ad44b-6bd5-4c99-9f68-4b6c7486f927 46 | nonce | \x3aa2e92f9808e496aa4163a59304b895 47 | created_at | 2022-12-14 02:29:21.3625+00 48 | updated_at | 2022-12-14 02:29:21.3625+00 49 | ``` 50 | 51 | There is also a handy function for creating secrets called 52 | `vault.create_secret()`: 53 | 54 | ``` 55 | postgres=> select vault.create_secret('another_s3kre3t'); 56 | -[ RECORD 1 ]-+------------------------------------- 57 | create_secret | c9b00867-ca8b-44fc-a81d-d20b8169be17 58 | 59 | ``` 60 | 61 | The function returns the UUID of the new secret. 62 | 63 | ## Name and Description 64 | 65 | Secrets can also have an optional _unique_ name, or an optional 66 | description. These are also arguments to `vault.create_secret()`: 67 | 68 | ``` 69 | postgres=> select vault.create_secret('another_s3kre3t', 'unique_name', 'This is the description'); 70 | -[ RECORD 1 ]-+------------------------------------- 71 | create_secret | 7095d222-efe5-4cd5-b5c6-5755b451e223 72 | 73 | postgres=> select * from vault.secrets where id = '7095d222-efe5-4cd5-b5c6-5755b451e223'; 74 | -[ RECORD 1 ]----------------------------------------------------------------- 75 | id | 7095d222-efe5-4cd5-b5c6-5755b451e223 76 | name | unique_name 77 | description | This is the description 78 | secret | 3mMeOcoG84a5F2uOfy2ugWYDp9sdxvCTmi6kTeT97bvA8rCEsG5DWWZtTU8VVeE= 79 | key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 80 | nonce | \x9f2d60954ba5eb566445736e0760b0e3 81 | created_at | 2022-12-14 02:34:23.85159+00 82 | updated_at | 2022-12-14 02:34:23.85159+00 83 | ``` 84 | 85 | ## Querying Data from the Vault 86 | 87 | If you look in the `vault.secrets` table, you will see that your data 88 | is stored encrypted. To decrypt the data, there is an automatically 89 | created view `vault.decrypted_secrets`. This view will decrypt secret 90 | data on the fly: 91 | 92 | ``` 93 | postgres=> select * from vault.decrypted_secrets order by created_at desc limit 3; 94 | -[ RECORD 1 ]----+----------------------------------------------------------------- 95 | id | 7095d222-efe5-4cd5-b5c6-5755b451e223 96 | name | unique_name 97 | description | This is the description 98 | secret | 3mMeOcoG84a5F2uOfy2ugWYDp9sdxvCTmi6kTeT97bvA8rCEsG5DWWZtTU8VVeE= 99 | decrypted_secret | another_s3kre3t 100 | key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 101 | nonce | \x9f2d60954ba5eb566445736e0760b0e3 102 | created_at | 2022-12-14 02:34:23.85159+00 103 | updated_at | 2022-12-14 02:34:23.85159+00 104 | -[ RECORD 2 ]----+----------------------------------------------------------------- 105 | id | c9b00867-ca8b-44fc-a81d-d20b8169be17 106 | name | 107 | description | 108 | secret | a1CE4vXwQ53+N9bllJj1D7fasm59ykohjb7K90PPsRFUd9IbBdxIGZNoSQLIXl4= 109 | decrypted_secret | another_s3kre3t 110 | key_id | 8c72b05e-b931-4372-abf9-a09cfad18489 111 | nonce | \x1d3b2761548c4efb2d29ca11d44aa22f 112 | created_at | 2022-12-14 02:32:50.58921+00 113 | updated_at | 2022-12-14 02:32:50.58921+00 114 | -[ RECORD 3 ]----+----------------------------------------------------------------- 115 | id | d91596b8-1047-446c-b9c0-66d98af6d001 116 | name | 117 | description | 118 | secret | S02eXS9BBY+kE3r621IS8beAytEEtj+dDHjs9/0AoMy7HTbog+ylxcS22A== 119 | decrypted_secret | s3kre3t_k3y 120 | key_id | 7f5ad44b-6bd5-4c99-9f68-4b6c7486f927 121 | nonce | \x3aa2e92f9808e496aa4163a59304b895 122 | created_at | 2022-12-14 02:29:21.3625+00 123 | updated_at | 2022-12-14 02:29:21.3625+00 124 | ``` 125 | 126 | Notice how this view has a `decrypted_secret` column that contains the 127 | decrypted secrets. Views are not stored on disk, they are only run at 128 | query time, so the secret remains encrypted on disk, and in any backup 129 | dumps or replication streams. 130 | 131 | You should ensure that you protect access to this view with the 132 | appropriate SQL privilege settings at all times, as anyone that has 133 | access to the view has access to decrypted secrets. 134 | 135 | ## Updating Secrets 136 | 137 | A secret can be updated with the `vault.update_secret()` function, 138 | this function makes updating secrets easy, just provide the secret 139 | UUID as the first argument, and then an updated secret, updated 140 | optional unique name, or updated description: 141 | 142 | ``` 143 | postgres=> select vault.update_secret('7095d222-efe5-4cd5-b5c6-5755b451e223', 'n3w_upd@ted_s3kret', 144 | 'updated_unique_name', 'This is the updated description'); 145 | -[ RECORD 1 ]-+- 146 | update_secret | 147 | 148 | postgres=> select * from vault.decrypted_secrets where id = '7095d222-efe5-4cd5-b5c6-5755b451e223'; 149 | -[ RECORD 1 ]----+--------------------------------------------------------------------- 150 | id | 7095d222-efe5-4cd5-b5c6-5755b451e223 151 | name | updated_unique_name 152 | description | This is the updated description 153 | secret | lhb3HBFxF+qJzp/HHCwhjl4QFb5dYDsIQEm35DaZQOovdkgp2iy6UMufTKJGH4ThMrU= 154 | decrypted_secret | n3w_upd@ted_s3kret 155 | key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 156 | nonce | \x9f2d60954ba5eb566445736e0760b0e3 157 | created_at | 2022-12-14 02:34:23.85159+00 158 | updated_at | 2022-12-14 02:51:13.938396+00 159 | ``` 160 | 161 | ## Internal Details 162 | 163 | To encrypt data, you need a *key id*. You can use the default key id 164 | created automatically for every project, or create your own key ids 165 | Using the `pgsodium.create_key()` function. Key ids are used to 166 | internally derive the encryption key used to encrypt secrets in the 167 | vault. Vault users typically do not have access to the key itself, 168 | only the key id. 169 | 170 | Both `vault.create_secret()` and `vault.update_secret()` take an 171 | optional fourth `new_key_id` argument. This argument can be used to 172 | store a different key id for the secret instead of the default value. 173 | 174 | ``` 175 | postgres=> select vault.create_secret('another_s3kre3t_key', 'another_unique_name', 176 | 'This is another description', (pgsodium.create_key()).id); 177 | -[ RECORD 1 ]-+------------------------------------- 178 | create_secret | cec9e005-a44d-4b19-86e1-febf3cd40619 179 | ``` 180 | 181 | Which roles should have access to the `vault.secrets` table should be 182 | carefully considered. There are two ways to grant access, the first 183 | is that the `postgres` user can explicitly grant access to the vault 184 | table itself. 185 | 186 | ## Turning off Statement Logging 187 | 188 | When you insert secrets into the vault table with an INSERT statement, 189 | those statements get logged by default into the Supabase logs. Since 190 | this would mean your secrets are stored unencrypted in the logs, you 191 | should turn off statement logging while using the Vault. 192 | 193 | While turning off statement logging does hinder you if you're used to 194 | looking at the logs to debug your application, it provides a much 195 | higher level of security by ensuring that your data does not leak out 196 | of the database and into the logs. This is especially critical with 197 | encrypted column data, because the statement logs will contain the 198 | *unencrypted* secrets. If you *must* store that data encrypted, then 199 | you *must* turn off statement logging. 200 | 201 | ``` 202 | ALTER SYSTEM SET statement_log = 'none'; 203 | ``` 204 | 205 | And then restart your project from the dashboard to enable that 206 | change. 207 | 208 | In the future we are researching various ways to refine the way 209 | statement logging interacts with sensitive columns. 210 | -------------------------------------------------------------------------------- /nix/pgScript.nix: -------------------------------------------------------------------------------- 1 | { postgresql, writeShellScriptBin } : 2 | 3 | let 4 | ver = builtins.head (builtins.splitVersion postgresql.version); 5 | src = builtins.readFile ./withTmpDb.sh.in; 6 | in 7 | (writeShellScriptBin "vault-with-pg-${ver}" src).overrideAttrs(old: { 8 | buildCommand = '' 9 | ${old.buildCommand} 10 | substituteInPlace $out/bin/${old.name} --subst-var-by 'POSTGRESQL_PATH' '${postgresql}' 11 | ''; 12 | }) 13 | -------------------------------------------------------------------------------- /nix/pgsodium.nix: -------------------------------------------------------------------------------- 1 | { lib, stdenv, fetchFromGitHub, libsodium, postgresql }: 2 | 3 | stdenv.mkDerivation rec { 4 | pname = "pgsodium"; 5 | version = "3.1.8"; 6 | 7 | buildInputs = [ libsodium postgresql ]; 8 | 9 | src = fetchFromGitHub { 10 | owner = "michelp"; 11 | repo = pname; 12 | rev = "refs/tags/v${version}"; 13 | hash = "sha256-j5F1PPdwfQRbV8XJ8Mloi8FvZF0MTl4eyIJcBYQy1E4="; 14 | }; 15 | 16 | installPhase = '' 17 | mkdir -p $out/{lib,share/postgresql/extension} 18 | 19 | install -D *${postgresql.dlSuffix} $out/lib 20 | install -D -t $out/share/postgresql/extension sql/*.sql 21 | install -D -t $out/share/postgresql/extension *.control 22 | ''; 23 | 24 | meta = with lib; { 25 | description = "Modern cryptography for PostgreSQL"; 26 | homepage = "https://github.com/michelp/${pname}"; 27 | maintainers = with maintainers; [ samrose ]; 28 | platforms = postgresql.meta.platforms; 29 | license = licenses.postgresql; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /nix/pgtap.nix: -------------------------------------------------------------------------------- 1 | { lib, stdenv, fetchFromGitHub, postgresql, perl, perlPackages, which }: 2 | 3 | stdenv.mkDerivation rec { 4 | pname = "pgtap"; 5 | version = "1.2.0"; 6 | 7 | src = fetchFromGitHub { 8 | owner = "theory"; 9 | repo = "pgtap"; 10 | rev = "v${version}"; 11 | hash = "sha256-lb0PRffwo6J5a6Hqw1ggvn0cW7gPZ02OEcLPi9ineI8="; 12 | }; 13 | 14 | nativeBuildInputs = [ postgresql perl perlPackages.TAPParserSourceHandlerpgTAP which ]; 15 | 16 | installPhase = '' 17 | install -D {sql/pgtap--${version}.sql,pgtap.control} -t $out/share/postgresql/extension 18 | ''; 19 | 20 | meta = with lib; { 21 | description = "A unit testing framework for PostgreSQL"; 22 | longDescription = '' 23 | pgTAP is a unit testing framework for PostgreSQL written in PL/pgSQL and PL/SQL. 24 | It includes a comprehensive collection of TAP-emitting assertion functions, 25 | as well as the ability to integrate with other TAP-emitting test frameworks. 26 | It can also be used in the xUnit testing style. 27 | ''; 28 | maintainers = with maintainers; [ samrose ]; 29 | homepage = "https://pgtap.org"; 30 | inherit (postgresql.meta) platforms; 31 | license = licenses.mit; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /nix/postgresql/17.nix: -------------------------------------------------------------------------------- 1 | import ./generic.nix { 2 | version = "17.0"; 3 | hash = "sha256-fidhMcD91rYliNutmzuyS4w0mNUAkyjbpZrxboGRCd4="; 4 | } 5 | -------------------------------------------------------------------------------- /nix/postgresql/default.nix: -------------------------------------------------------------------------------- 1 | self: 2 | let 3 | # Before removing an EOL major version, make sure to check the versioning policy in: 4 | # /nixos/modules/services/databases/postgresql.md 5 | # 6 | # Before removing, make sure to update it to the last minor version - and if only in 7 | # an immediately preceding commit. This allows people relying on that old major version 8 | # for a bit longer to still update up to this commit to at least get the latest minor 9 | # version. In other words: Do not remove the second-to-last minor version from nixpkgs, 10 | # yet. Update first. 11 | versions = { 12 | postgresql_17 = ./17.nix; 13 | }; 14 | 15 | mkAttributes = jitSupport: 16 | self.lib.mapAttrs' (version: path: 17 | let 18 | attrName = if jitSupport then "${version}_jit" else version; 19 | in 20 | self.lib.nameValuePair attrName (import path { 21 | inherit jitSupport self; 22 | }) 23 | ) versions; 24 | 25 | in 26 | # variations without and with JIT 27 | (mkAttributes false) // (mkAttributes true) 28 | -------------------------------------------------------------------------------- /nix/postgresql/generic.nix: -------------------------------------------------------------------------------- 1 | let 2 | 3 | generic = 4 | # dependencies 5 | { stdenv, lib, fetchurl, makeWrapper 6 | , glibc, zlib, readline, openssl, icu, lz4, zstd, systemd, libossp_uuid 7 | , pkg-config, libxml2, tzdata, libkrb5, substituteAll, darwin 8 | , linux-pam 9 | , bison, flex, perl, docbook_xml_dtd_45, docbook-xsl-nons, libxslt 10 | 11 | # This is important to obtain a version of `libpq` that does not depend on systemd. 12 | , systemdSupport ? lib.meta.availableOn stdenv.hostPlatform systemd && !stdenv.hostPlatform.isStatic 13 | , enableSystemd ? null 14 | , gssSupport ? with stdenv.hostPlatform; !isWindows && !isStatic 15 | 16 | # for postgresql.pkgs 17 | , self, newScope, buildEnv 18 | 19 | # source specification 20 | , version, hash, muslPatches ? {} 21 | 22 | # for tests 23 | , testers, nixosTests 24 | 25 | # JIT 26 | , jitSupport 27 | , nukeReferences, patchelf, llvmPackages 28 | 29 | # PL/Python 30 | , pythonSupport ? false 31 | , python3 32 | 33 | # detection of crypt fails when using llvm stdenv, so we add it manually 34 | # for <13 (where it got removed: https://github.com/postgres/postgres/commit/c45643d618e35ec2fe91438df15abd4f3c0d85ca) 35 | , libxcrypt 36 | } @args: 37 | let 38 | atLeast = lib.versionAtLeast version; 39 | olderThan = lib.versionOlder version; 40 | lz4Enabled = atLeast "14"; 41 | zstdEnabled = atLeast "15"; 42 | 43 | systemdSupport' = if enableSystemd == null then systemdSupport else (lib.warn "postgresql: argument enableSystemd is deprecated, please use systemdSupport instead." enableSystemd); 44 | 45 | pname = "postgresql"; 46 | 47 | stdenv' = if jitSupport then llvmPackages.stdenv else stdenv; 48 | in stdenv'.mkDerivation (finalAttrs: { 49 | inherit version; 50 | pname = pname + lib.optionalString jitSupport "-jit"; 51 | 52 | src = fetchurl { 53 | url = "mirror://postgresql/source/v${version}/${pname}-${version}.tar.bz2"; 54 | inherit hash; 55 | }; 56 | 57 | hardeningEnable = lib.optionals (!stdenv'.cc.isClang) [ "pie" ]; 58 | 59 | outputs = [ "out" "lib" "doc" "man" ]; 60 | setOutputFlags = false; # $out retains configureFlags :-/ 61 | 62 | buildInputs = [ 63 | zlib 64 | readline 65 | openssl 66 | libxml2 67 | icu 68 | ] 69 | ++ lib.optionals (olderThan "13") [ libxcrypt ] 70 | ++ lib.optionals jitSupport [ llvmPackages.llvm ] 71 | ++ lib.optionals lz4Enabled [ lz4 ] 72 | ++ lib.optionals zstdEnabled [ zstd ] 73 | ++ lib.optionals systemdSupport' [ systemd ] 74 | ++ lib.optionals pythonSupport [ python3 ] 75 | ++ lib.optionals gssSupport [ libkrb5 ] 76 | ++ lib.optionals stdenv'.isLinux [ linux-pam ] 77 | ++ lib.optionals (!stdenv'.isDarwin) [ libossp_uuid ]; 78 | 79 | nativeBuildInputs = [ 80 | makeWrapper 81 | pkg-config 82 | ] 83 | ++ lib.optionals jitSupport [ llvmPackages.llvm.dev nukeReferences patchelf ] 84 | ++ lib.optionals (atLeast "17") [ bison flex perl docbook_xml_dtd_45 docbook-xsl-nons libxslt ]; 85 | 86 | enableParallelBuilding = true; 87 | 88 | separateDebugInfo = true; 89 | 90 | buildFlags = [ "world" ]; 91 | 92 | # Makes cross-compiling work when xml2-config can't be executed on the host. 93 | # Fixed upstream in https://github.com/postgres/postgres/commit/0bc8cebdb889368abdf224aeac8bc197fe4c9ae6 94 | env.NIX_CFLAGS_COMPILE = lib.optionalString (olderThan "13") "-I${libxml2.dev}/include/libxml2"; 95 | 96 | configureFlags = [ 97 | "--with-openssl" 98 | "--with-libxml" 99 | "--with-icu" 100 | "--sysconfdir=/etc" 101 | "--libdir=$(lib)/lib" 102 | "--with-system-tzdata=${tzdata}/share/zoneinfo" 103 | "--enable-debug" 104 | (lib.optionalString systemdSupport' "--with-systemd") 105 | (if stdenv'.isDarwin then "--with-uuid=e2fs" else "--with-ossp-uuid") 106 | ] ++ lib.optionals lz4Enabled [ "--with-lz4" ] 107 | ++ lib.optionals zstdEnabled [ "--with-zstd" ] 108 | ++ lib.optionals gssSupport [ "--with-gssapi" ] 109 | ++ lib.optionals pythonSupport [ "--with-python" ] 110 | ++ lib.optionals jitSupport [ "--with-llvm" ] 111 | ++ lib.optionals stdenv'.isLinux [ "--with-pam" ]; 112 | 113 | patches = [ 114 | (if atLeast "16" then ./patches/relative-to-symlinks-16+.patch else ./patches/relative-to-symlinks.patch) 115 | ./patches/less-is-more.patch 116 | ./patches/paths-for-split-outputs.patch 117 | ./patches/specify_pkglibdir_at_runtime.patch 118 | ./patches/paths-with-postgresql-suffix.patch 119 | 120 | (substituteAll { 121 | src = ./patches/locale-binary-path.patch; 122 | locale = "${if stdenv.isDarwin then darwin.adv_cmds else lib.getBin stdenv.cc.libc}/bin/locale"; 123 | }) 124 | 125 | ] ++ lib.optionals stdenv'.hostPlatform.isMusl ( 126 | # Using fetchurl instead of fetchpatch on purpose: https://github.com/NixOS/nixpkgs/issues/240141 127 | map fetchurl (lib.attrValues muslPatches) 128 | ) ++ lib.optionals stdenv'.isLinux [ 129 | (if atLeast "13" then ./patches/socketdir-in-run-13+.patch else ./patches/socketdir-in-run.patch) 130 | ]; 131 | 132 | installTargets = [ "install-world" ]; 133 | 134 | postPatch = '' 135 | # Hardcode the path to pgxs so pg_config returns the path in $out 136 | substituteInPlace "src/common/config_info.c" --subst-var out 137 | '' + lib.optionalString jitSupport '' 138 | # Force lookup of jit stuff in $out instead of $lib 139 | substituteInPlace src/backend/jit/jit.c --replace pkglib_path \"$out/lib\" 140 | substituteInPlace src/backend/jit/llvm/llvmjit.c --replace pkglib_path \"$out/lib\" 141 | substituteInPlace src/backend/jit/llvm/llvmjit_inline.cpp --replace pkglib_path \"$out/lib\" 142 | ''; 143 | 144 | postInstall = 145 | '' 146 | moveToOutput "lib/pgxs" "$out" # looks strange, but not deleting it 147 | moveToOutput "lib/libpgcommon*.a" "$out" 148 | moveToOutput "lib/libpgport*.a" "$out" 149 | moveToOutput "lib/libecpg*" "$out" 150 | 151 | # Prevent a retained dependency on gcc-wrapper. 152 | substituteInPlace "$out/lib/pgxs/src/Makefile.global" --replace ${stdenv'.cc}/bin/ld ld 153 | 154 | if [ -z "''${dontDisableStatic:-}" ]; then 155 | # Remove static libraries in case dynamic are available. 156 | for i in $out/lib/*.a $lib/lib/*.a; do 157 | name="$(basename "$i")" 158 | ext="${stdenv'.hostPlatform.extensions.sharedLibrary}" 159 | if [ -e "$lib/lib/''${name%.a}$ext" ] || [ -e "''${i%.a}$ext" ]; then 160 | rm "$i" 161 | fi 162 | done 163 | fi 164 | '' + lib.optionalString jitSupport '' 165 | # Move the bitcode and libllvmjit.so library out of $lib; otherwise, every client that 166 | # depends on libpq.so will also have libLLVM.so in its closure too, bloating it 167 | moveToOutput "lib/bitcode" "$out" 168 | moveToOutput "lib/llvmjit*" "$out" 169 | 170 | # In the case of JIT support, prevent a retained dependency on clang-wrapper 171 | substituteInPlace "$out/lib/pgxs/src/Makefile.global" --replace ${stdenv'.cc}/bin/clang clang 172 | nuke-refs $out/lib/llvmjit_types.bc $(find $out/lib/bitcode -type f) 173 | 174 | # Stop out depending on the default output of llvm 175 | substituteInPlace $out/lib/pgxs/src/Makefile.global \ 176 | --replace ${llvmPackages.llvm.out}/bin "" \ 177 | --replace '$(LLVM_BINPATH)/' "" 178 | 179 | # Stop out depending on the -dev output of llvm 180 | substituteInPlace $out/lib/pgxs/src/Makefile.global \ 181 | --replace ${llvmPackages.llvm.dev}/bin/llvm-config llvm-config \ 182 | --replace -I${llvmPackages.llvm.dev}/include "" 183 | 184 | ${lib.optionalString (!stdenv'.isDarwin) '' 185 | # Stop lib depending on the -dev output of llvm 186 | rpath=$(patchelf --print-rpath $out/lib/llvmjit.so) 187 | nuke-refs -e $out $out/lib/llvmjit.so 188 | # Restore the correct rpath 189 | patchelf $out/lib/llvmjit.so --set-rpath "$rpath" 190 | ''} 191 | ''; 192 | 193 | postFixup = lib.optionalString (!stdenv'.isDarwin && stdenv'.hostPlatform.libc == "glibc") 194 | '' 195 | # initdb needs access to "locale" command from glibc. 196 | wrapProgram $out/bin/initdb --prefix PATH ":" ${glibc.bin}/bin 197 | ''; 198 | 199 | doCheck = false; 200 | # autodetection doesn't seem to able to find this, but it's there. 201 | checkTarget = "check"; 202 | 203 | disallowedReferences = [ stdenv'.cc ]; 204 | 205 | passthru = let 206 | this = self.callPackage generic args; 207 | jitToggle = this.override { 208 | jitSupport = !jitSupport; 209 | }; 210 | in 211 | { 212 | psqlSchema = lib.versions.major version; 213 | 214 | withJIT = if jitSupport then this else jitToggle; 215 | withoutJIT = if jitSupport then jitToggle else this; 216 | 217 | dlSuffix = if olderThan "16" then ".so" else stdenv.hostPlatform.extensions.sharedLibrary; 218 | 219 | pkgs = let 220 | scope = { 221 | inherit jitSupport; 222 | inherit (llvmPackages) llvm; 223 | postgresql = this; 224 | stdenv = stdenv'; 225 | }; 226 | newSelf = self // scope; 227 | newSuper = { callPackage = newScope (scope // this.pkgs); }; 228 | in import ./ext newSelf newSuper; 229 | 230 | withPackages = postgresqlWithPackages { 231 | inherit makeWrapper buildEnv; 232 | postgresql = this; 233 | } 234 | this.pkgs; 235 | 236 | tests = { 237 | postgresql-wal-receiver = import ../../../../nixos/tests/postgresql-wal-receiver.nix { 238 | inherit (stdenv) system; 239 | pkgs = self; 240 | package = this; 241 | }; 242 | pkg-config = testers.testMetaPkgConfig finalAttrs.finalPackage; 243 | } // lib.optionalAttrs jitSupport { 244 | postgresql-jit = import ../../../../nixos/tests/postgresql-jit.nix { 245 | inherit (stdenv) system; 246 | pkgs = self; 247 | package = this; 248 | }; 249 | }; 250 | }; 251 | 252 | meta = with lib; { 253 | homepage = "https://www.postgresql.org"; 254 | description = "Powerful, open source object-relational database system"; 255 | license = licenses.postgresql; 256 | changelog = "https://www.postgresql.org/docs/release/${finalAttrs.version}/"; 257 | maintainers = with maintainers; [ thoughtpolice danbst globin ivan ma27 wolfgangwalther ]; 258 | pkgConfigModules = [ "libecpg" "libecpg_compat" "libpgtypes" "libpq" ]; 259 | platforms = platforms.unix; 260 | 261 | # JIT support doesn't work with cross-compilation. It is attempted to build LLVM-bytecode 262 | # (`%.bc` is the corresponding `make(1)`-rule) for each sub-directory in `backend/` for 263 | # the JIT apparently, but with a $(CLANG) that can produce binaries for the build, not the 264 | # host-platform. 265 | # 266 | # I managed to get a cross-build with JIT support working with 267 | # `depsBuildBuild = [ llvmPackages.clang ] ++ buildInputs`, but considering that the 268 | # resulting LLVM IR isn't platform-independent this doesn't give you much. 269 | # In fact, I tried to test the result in a VM-test, but as soon as JIT was used to optimize 270 | # a query, postgres would coredump with `Illegal instruction`. 271 | broken = (jitSupport && stdenv.hostPlatform != stdenv.buildPlatform) 272 | # Allmost all tests fail FATAL errors for v12 and v13 273 | || (jitSupport && stdenv.hostPlatform.isMusl && olderThan "14"); 274 | }; 275 | }); 276 | 277 | postgresqlWithPackages = { postgresql, makeWrapper, buildEnv }: pkgs: f: buildEnv { 278 | name = "postgresql-and-plugins-${postgresql.version}"; 279 | paths = f pkgs ++ [ 280 | postgresql 281 | postgresql.lib 282 | postgresql.man # in case user installs this into environment 283 | ]; 284 | nativeBuildInputs = [ makeWrapper ]; 285 | 286 | 287 | # We include /bin to ensure the $out/bin directory is created, which is 288 | # needed because we'll be removing the files from that directory in postBuild 289 | # below. See #22653 290 | pathsToLink = ["/" "/bin"]; 291 | 292 | # Note: the duplication of executables is about 4MB size. 293 | # So a nicer solution was patching postgresql to allow setting the 294 | # libdir explicitly. 295 | postBuild = '' 296 | mkdir -p $out/bin 297 | rm $out/bin/{pg_config,postgres,pg_ctl} 298 | cp --target-directory=$out/bin ${postgresql}/bin/{postgres,pg_config,pg_ctl} 299 | wrapProgram $out/bin/postgres --set NIX_PGLIBDIR $out/lib 300 | ''; 301 | 302 | passthru.version = postgresql.version; 303 | passthru.psqlSchema = postgresql.psqlSchema; 304 | }; 305 | 306 | in 307 | # passed by .nix 308 | versionArgs: 309 | # passed by default.nix 310 | { self, ... } @defaultArgs: 311 | self.callPackage generic (defaultArgs // versionArgs) 312 | -------------------------------------------------------------------------------- /nix/postgresql/patches/less-is-more.patch: -------------------------------------------------------------------------------- 1 | --- a/src/include/fe_utils/print.h 2 | +++ b/src/include/fe_utils/print.h 3 | @@ -18,7 +18,7 @@ 4 | 5 | /* This is not a particularly great place for this ... */ 6 | #ifndef __CYGWIN__ 7 | -#define DEFAULT_PAGER "more" 8 | +#define DEFAULT_PAGER "less" 9 | #else 10 | #define DEFAULT_PAGER "less" 11 | #endif 12 | -------------------------------------------------------------------------------- /nix/postgresql/patches/locale-binary-path.patch: -------------------------------------------------------------------------------- 1 | --- a/src/backend/commands/collationcmds.c 2 | +++ b/src/backend/commands/collationcmds.c 3 | @@ -611,7 +611,7 @@ pg_import_system_collations(PG_FUNCTION_ARGS) 4 | aliases = (CollAliasData *) palloc(maxaliases * sizeof(CollAliasData)); 5 | naliases = 0; 6 | 7 | - locale_a_handle = OpenPipeStream("locale -a", "r"); 8 | + locale_a_handle = OpenPipeStream("@locale@ -a", "r"); 9 | if (locale_a_handle == NULL) 10 | ereport(ERROR, 11 | (errcode_for_file_access(), 12 | -------------------------------------------------------------------------------- /nix/postgresql/patches/paths-for-split-outputs.patch: -------------------------------------------------------------------------------- 1 | --- a/src/common/config_info.c 2 | +++ b/src/common/config_info.c 3 | @@ -118,7 +118,7 @@ 4 | i++; 5 | 6 | configdata[i].name = pstrdup("PGXS"); 7 | + strlcpy(path, "@out@/lib", sizeof(path)); 8 | - get_pkglib_path(my_exec_path, path); 9 | strlcat(path, "/pgxs/src/makefiles/pgxs.mk", sizeof(path)); 10 | cleanup_path(path); 11 | configdata[i].setting = pstrdup(path); 12 | -------------------------------------------------------------------------------- /nix/postgresql/patches/paths-with-postgresql-suffix.patch: -------------------------------------------------------------------------------- 1 | Nix outputs put the `name' in each store path like 2 | /nix/store/...-. This was confusing the Postgres make script 3 | because it thought its data directory already had postgresql in its 4 | directory. This lead to Postgres installing all of its fils in 5 | $out/share. To fix this, we just look for postgres or psql in the part 6 | after the / using make's notdir. 7 | 8 | --- 9 | --- a/src/Makefile.global.in 10 | +++ b/src/Makefile.global.in 11 | @@ -102,15 +102,15 @@ datarootdir := @datarootdir@ 12 | bindir := @bindir@ 13 | 14 | datadir := @datadir@ 15 | -ifeq "$(findstring pgsql, $(datadir))" "" 16 | -ifeq "$(findstring postgres, $(datadir))" "" 17 | +ifeq "$(findstring pgsql, $(notdir $(datadir)))" "" 18 | +ifeq "$(findstring postgres, $(notdir $(datadir)))" "" 19 | override datadir := $(datadir)/postgresql 20 | endif 21 | endif 22 | 23 | sysconfdir := @sysconfdir@ 24 | -ifeq "$(findstring pgsql, $(sysconfdir))" "" 25 | -ifeq "$(findstring postgres, $(sysconfdir))" "" 26 | +ifeq "$(findstring pgsql, $(notdir $(sysconfdir)))" "" 27 | +ifeq "$(findstring postgres, $(notdir $(sysconfdir)))" "" 28 | override sysconfdir := $(sysconfdir)/postgresql 29 | endif 30 | endif 31 | @@ -136,8 +136,8 @@ endif 32 | mandir := @mandir@ 33 | 34 | docdir := @docdir@ 35 | -ifeq "$(findstring pgsql, $(docdir))" "" 36 | -ifeq "$(findstring postgres, $(docdir))" "" 37 | +ifeq "$(findstring pgsql, $(notdir $(docdir)))" "" 38 | +ifeq "$(findstring postgres, $(notdir $(docdir)))" "" 39 | override docdir := $(docdir)/postgresql 40 | endif 41 | endif 42 | -------------------------------------------------------------------------------- /nix/postgresql/patches/relative-to-symlinks-16+.patch: -------------------------------------------------------------------------------- 1 | On NixOS we *want* stuff relative to symlinks. 2 | --- 3 | --- a/src/common/exec.c 4 | +++ b/src/common/exec.c 5 | @@ -238,6 +238,8 @@ 6 | static int 7 | normalize_exec_path(char *path) 8 | { 9 | + return 0; 10 | + 11 | /* 12 | * We used to do a lot of work ourselves here, but now we just let 13 | * realpath(3) do all the heavy lifting. 14 | -------------------------------------------------------------------------------- /nix/postgresql/patches/relative-to-symlinks.patch: -------------------------------------------------------------------------------- 1 | On NixOS we *want* stuff relative to symlinks. 2 | --- 3 | --- a/src/common/exec.c 4 | +++ b/src/common/exec.c 5 | @@ -218,6 +218,8 @@ 6 | static int 7 | resolve_symlinks(char *path) 8 | { 9 | + return 0; 10 | + 11 | #ifdef HAVE_READLINK 12 | struct stat buf; 13 | char orig_wd[MAXPGPATH], 14 | -------------------------------------------------------------------------------- /nix/postgresql/patches/socketdir-in-run-13+.patch: -------------------------------------------------------------------------------- 1 | --- a/src/include/pg_config_manual.h 2 | +++ b/src/include/pg_config_manual.h 3 | @@ -201,7 +201,7 @@ 4 | * support them yet. 5 | */ 6 | #ifndef WIN32 7 | -#define DEFAULT_PGSOCKET_DIR "/tmp" 8 | +#define DEFAULT_PGSOCKET_DIR "/run/postgresql" 9 | #else 10 | #define DEFAULT_PGSOCKET_DIR "" 11 | #endif 12 | -------------------------------------------------------------------------------- /nix/postgresql/patches/socketdir-in-run.patch: -------------------------------------------------------------------------------- 1 | --- a/src/include/pg_config_manual.h 2 | +++ b/src/include/pg_config_manual.h 3 | @@ -179,7 +179,7 @@ 4 | * here's where to twiddle it. You can also override this at runtime 5 | * with the postmaster's -k switch. 6 | */ 7 | -#define DEFAULT_PGSOCKET_DIR "/tmp" 8 | +#define DEFAULT_PGSOCKET_DIR "/run/postgresql" 9 | 10 | /* 11 | * This is the default event source for Windows event log. 12 | -------------------------------------------------------------------------------- /nix/postgresql/patches/specify_pkglibdir_at_runtime.patch: -------------------------------------------------------------------------------- 1 | --- a/src/port/path.c 2 | +++ b/src/port/path.c 3 | @@ -714,7 +714,11 @@ 4 | void 5 | get_lib_path(const char *my_exec_path, char *ret_path) 6 | { 7 | - make_relative_path(ret_path, LIBDIR, PGBINDIR, my_exec_path); 8 | + char const * const nix_pglibdir = getenv("NIX_PGLIBDIR"); 9 | + if(nix_pglibdir == NULL) 10 | + make_relative_path(ret_path, LIBDIR, PGBINDIR, my_exec_path); 11 | + else 12 | + make_relative_path(ret_path, nix_pglibdir, PGBINDIR, my_exec_path); 13 | } 14 | 15 | /* 16 | @@ -723,7 +727,11 @@ 17 | void 18 | get_pkglib_path(const char *my_exec_path, char *ret_path) 19 | { 20 | - make_relative_path(ret_path, PKGLIBDIR, PGBINDIR, my_exec_path); 21 | + char const * const nix_pglibdir = getenv("NIX_PGLIBDIR"); 22 | + if(nix_pglibdir == NULL) 23 | + make_relative_path(ret_path, PKGLIBDIR, PGBINDIR, my_exec_path); 24 | + else 25 | + make_relative_path(ret_path, nix_pglibdir, PGBINDIR, my_exec_path); 26 | } 27 | 28 | /* 29 | -------------------------------------------------------------------------------- /nix/supabase_vault.nix: -------------------------------------------------------------------------------- 1 | { stdenv, libsodium, postgresql }: 2 | 3 | stdenv.mkDerivation rec { 4 | name = "supabase_vault"; 5 | 6 | buildInputs = [ libsodium postgresql ]; 7 | 8 | src = ../.; 9 | 10 | installPhase = '' 11 | mkdir -p $out/{lib,share/postgresql/extension} 12 | 13 | install -D *${postgresql.dlSuffix} $out/lib 14 | install -D -t $out/share/postgresql/extension sql/*.sql 15 | install -D -t $out/share/postgresql/extension *.control 16 | ''; 17 | } 18 | -------------------------------------------------------------------------------- /nix/withTmpDb.sh.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | export PATH=@POSTGRESQL_PATH@/bin:"$PATH" 6 | 7 | tmpdir="$(mktemp -d)" 8 | 9 | export PGDATA="$tmpdir" 10 | export PGHOST="$tmpdir" 11 | export PGUSER=postgres 12 | export PGDATABASE=postgres 13 | 14 | trap 'pg_ctl stop -m i && rm -rf "$tmpdir"' sigint sigterm exit 15 | 16 | PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" 17 | VAULT_GETKEY_SCRIPT_PATH="$tmpdir/vault_getkey.sh" 18 | 19 | options="-F -c listen_addresses=\"\" -c shared_preload_libraries=pgsodium,supabase_vault -c pgsodium.getkey_script=$VAULT_GETKEY_SCRIPT_PATH -c vault.getkey_script=$VAULT_GETKEY_SCRIPT_PATH -k $PGDATA" 20 | 21 | echo "echo 0000000000000000000000000000000000000000000000000000000000000000" > "$VAULT_GETKEY_SCRIPT_PATH" 22 | chmod +x "$VAULT_GETKEY_SCRIPT_PATH" 23 | 24 | pg_ctl start -o "$options" 25 | 26 | createdb contrib_regression 27 | 28 | psql -v ON_ERROR_STOP=1 -f test/fixtures.sql -d contrib_regression 29 | 30 | "$@" 31 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import (builtins.fetchTarball { 2 | name = "24.05"; # May 31 2024 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/24.05.tar.gz"; 4 | sha256 = "sha256:1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; 5 | }) {}; 6 | mkShell { 7 | buildInputs = 8 | let 9 | ourPg = callPackage ./nix/postgresql { 10 | inherit lib; 11 | inherit stdenv; 12 | inherit fetchurl; 13 | inherit makeWrapper; 14 | inherit callPackage; 15 | }; 16 | supportedPgVersions = [ 17 | postgresql_13 18 | postgresql_14 19 | postgresql_15 20 | postgresql_16 21 | ourPg.postgresql_17 22 | ]; 23 | pgWithExt = { pg }: pg.withPackages (p: [ 24 | (callPackage ./nix/pgsodium.nix { postgresql = pg; }) 25 | (callPackage ./nix/supabase_vault.nix { postgresql = pg; }) 26 | (callPackage ./nix/pgtap.nix { postgresql = pg; }) 27 | ]); 28 | extAll = map (x: callPackage ./nix/pgScript.nix { postgresql = pgWithExt { pg = x; }; }) supportedPgVersions; 29 | in 30 | [ 31 | extAll 32 | ]; 33 | shellHook = '' 34 | export HISTFILE=.history 35 | ''; 36 | } 37 | -------------------------------------------------------------------------------- /sql/supabase_vault--0.2.8--0.3.0.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION vault._crypto_aead_det_encrypt(message bytea, additional bytea, key_id bigint, context bytea = 'pgsodium', nonce bytea = NULL) 2 | RETURNS bytea 3 | AS 'MODULE_PATHNAME', 'pgsodium_crypto_aead_det_encrypt_by_id' 4 | LANGUAGE c IMMUTABLE; 5 | 6 | CREATE OR REPLACE FUNCTION vault._crypto_aead_det_decrypt(message bytea, additional bytea, key_id bigint, context bytea = 'pgsodium', nonce bytea = NULL) 7 | RETURNS bytea 8 | AS 'MODULE_PATHNAME', 'pgsodium_crypto_aead_det_decrypt_by_id' 9 | LANGUAGE c IMMUTABLE; 10 | 11 | CREATE OR REPLACE FUNCTION vault._crypto_aead_det_noncegen() 12 | RETURNS bytea 13 | AS 'MODULE_PATHNAME', 'pgsodium_crypto_aead_det_noncegen' 14 | LANGUAGE c IMMUTABLE; 15 | 16 | ALTER TABLE vault.secrets OWNER TO current_user; 17 | 18 | SECURITY LABEL ON COLUMN vault.secrets.secret IS NULL; 19 | 20 | DROP TRIGGER IF EXISTS secrets_encrypt_secret_trigger_secret ON vault.secrets; 21 | DROP FUNCTION IF EXISTS vault.secrets_encrypt_secret_secret; 22 | 23 | ALTER TABLE vault.secrets DROP CONSTRAINT IF EXISTS secrets_key_id_fkey; 24 | ALTER TABLE vault.secrets ALTER key_id DROP DEFAULT; 25 | ALTER TABLE vault.secrets ALTER nonce SET DEFAULT vault._crypto_aead_det_noncegen(); 26 | 27 | DO $$ 28 | BEGIN 29 | SET search_path = ''; 30 | 31 | IF EXISTS (SELECT FROM vault.secrets) THEN 32 | UPDATE vault.decrypted_secrets s 33 | SET 34 | secret = encode( 35 | vault._crypto_aead_det_encrypt( 36 | message := convert_to(decrypted_secret, 'utf8'), 37 | additional := convert_to(s.id::text, 'utf8'), 38 | key_id := 0, 39 | context := 'pgsodium'::bytea, 40 | nonce := s.nonce 41 | ), 42 | 'base64' 43 | ), 44 | key_id = NULL; 45 | END IF; 46 | END 47 | $$; 48 | 49 | DROP VIEW IF EXISTS vault.decrypted_secrets; 50 | CREATE VIEW vault.decrypted_secrets AS 51 | SELECT s.id, 52 | s.name, 53 | s.description, 54 | s.secret, 55 | convert_from( 56 | vault._crypto_aead_det_decrypt( 57 | message := decode(s.secret, 'base64'::text), 58 | additional := convert_to(s.id::text, 'utf8'), 59 | key_id := 0, 60 | context := 'pgsodium'::bytea, 61 | nonce := s.nonce 62 | ), 63 | 'utf8'::name 64 | ) AS decrypted_secret, 65 | s.key_id, 66 | s.nonce, 67 | s.created_at, 68 | s.updated_at 69 | FROM vault.secrets s; 70 | 71 | CREATE OR REPLACE FUNCTION vault.create_secret( 72 | new_secret text, 73 | new_name text = NULL, 74 | new_description text = '', 75 | -- unused 76 | new_key_id uuid = NULL 77 | ) 78 | RETURNS uuid 79 | SECURITY DEFINER 80 | LANGUAGE plpgsql 81 | SET search_path = '' 82 | AS $$ 83 | DECLARE 84 | rec record; 85 | BEGIN 86 | INSERT INTO vault.secrets (secret, name, description) 87 | VALUES ( 88 | new_secret, 89 | new_name, 90 | new_description 91 | ) 92 | RETURNING * INTO rec; 93 | UPDATE vault.secrets s 94 | SET secret = encode(vault._crypto_aead_det_encrypt( 95 | message := convert_to(rec.secret, 'utf8'), 96 | additional := convert_to(s.id::text, 'utf8'), 97 | key_id := 0, 98 | context := 'pgsodium'::bytea, 99 | nonce := rec.nonce 100 | ), 'base64') 101 | WHERE id = rec.id; 102 | RETURN rec.id; 103 | END 104 | $$; 105 | 106 | CREATE OR REPLACE FUNCTION vault.update_secret( 107 | secret_id uuid, 108 | new_secret text = NULL, 109 | new_name text = NULL, 110 | new_description text = NULL, 111 | -- unused 112 | new_key_id uuid = NULL 113 | ) 114 | RETURNS void 115 | SECURITY DEFINER 116 | LANGUAGE plpgsql 117 | SET search_path = '' 118 | AS $$ 119 | DECLARE 120 | decrypted_secret text := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE id = secret_id); 121 | BEGIN 122 | UPDATE vault.secrets s 123 | SET 124 | secret = CASE WHEN new_secret IS NULL THEN s.secret 125 | ELSE encode(vault._crypto_aead_det_encrypt( 126 | message := convert_to(new_secret, 'utf8'), 127 | additional := convert_to(s.id::text, 'utf8'), 128 | key_id := 0, 129 | context := 'pgsodium'::bytea, 130 | nonce := s.nonce 131 | ), 'base64') END, 132 | name = coalesce(new_name, s.name), 133 | description = coalesce(new_description, s.description), 134 | updated_at = now() 135 | WHERE s.id = secret_id; 136 | END 137 | $$; 138 | 139 | REVOKE ALL ON SCHEMA vault FROM pgsodium_keyiduser; 140 | REVOKE ALL ON vault.decrypted_secrets, vault.secrets FROM pgsodium_keyiduser; 141 | 142 | REVOKE ALL ON FUNCTION 143 | vault._crypto_aead_det_encrypt, 144 | vault._crypto_aead_det_decrypt, 145 | vault._crypto_aead_det_noncegen, 146 | vault.create_secret, 147 | vault.update_secret 148 | FROM PUBLIC; 149 | -------------------------------------------------------------------------------- /sql/supabase_vault--0.2.8.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE vault.secrets ( 2 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 3 | name text, 4 | description text NOT NULL default '', 5 | secret text NOT NULL, 6 | key_id uuid REFERENCES pgsodium.key(id) DEFAULT (pgsodium.create_key()).id, 7 | nonce bytea DEFAULT pgsodium.crypto_aead_det_noncegen(), 8 | created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | ALTER TABLE vault.secrets OWNER TO session_user; 12 | 13 | COMMENT ON TABLE vault.secrets IS 'Table with encrypted `secret` column for storing sensitive information on disk.'; 14 | 15 | CREATE UNIQUE INDEX ON vault.secrets USING btree (name) WHERE name IS NOT NULL; 16 | 17 | SECURITY LABEL FOR pgsodium ON COLUMN vault.secrets.secret IS 18 | 'ENCRYPT WITH KEY COLUMN key_id ASSOCIATED (id, description, created_at, updated_at) NONCE nonce'; 19 | 20 | SELECT pgsodium.update_mask('vault.secrets'::regclass::oid); 21 | 22 | ALTER EXTENSION supabase_vault DROP VIEW vault.decrypted_secrets; 23 | ALTER EXTENSION supabase_vault DROP FUNCTION vault.secrets_encrypt_secret_secret; 24 | 25 | GRANT ALL ON SCHEMA vault TO pgsodium_keyiduser; 26 | GRANT ALL ON TABLE vault.secrets TO pgsodium_keyiduser; 27 | GRANT ALL PRIVILEGES ON vault.decrypted_secrets TO pgsodium_keyiduser; 28 | 29 | CREATE OR REPLACE FUNCTION vault.create_secret( 30 | new_secret text, 31 | new_name text = NULL, 32 | new_description text = '', 33 | new_key_id uuid = NULL) RETURNS uuid AS 34 | $$ 35 | INSERT INTO vault.secrets (secret, name, description, key_id) 36 | VALUES ( 37 | new_secret, 38 | new_name, 39 | new_description, 40 | CASE WHEN new_key_id IS NULL THEN (pgsodium.create_key()).id ELSE new_key_id END) 41 | RETURNING id; 42 | $$ LANGUAGE SQL; 43 | 44 | CREATE OR REPLACE FUNCTION vault.update_secret( 45 | secret_id uuid, 46 | new_secret text = NULL, 47 | new_name text = NULL, 48 | new_description text = NULL, 49 | new_key_id uuid = NULL) RETURNS void AS 50 | $$ 51 | UPDATE vault.decrypted_secrets s 52 | SET 53 | secret = CASE WHEN new_secret IS NULL THEN s.decrypted_secret ELSE new_secret END, 54 | name = CASE WHEN new_name IS NULL THEN s.name ELSE new_name END, 55 | description = CASE WHEN new_description IS NULL THEN s.description ELSE new_description END, 56 | key_id = CASE WHEN new_key_id IS NULL THEN s.key_id ELSE new_key_id END, 57 | updated_at = CURRENT_TIMESTAMP 58 | WHERE s.id = secret_id 59 | $$ LANGUAGE SQL; 60 | 61 | SELECT pg_catalog.pg_extension_config_dump('vault.secrets', ''); 62 | -------------------------------------------------------------------------------- /sql/supabase_vault--0.3.0--0.3.1.sql: -------------------------------------------------------------------------------- 1 | -- no SQL changes in 0.3.1 2 | -------------------------------------------------------------------------------- /sql/supabase_vault--0.3.0.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION vault._crypto_aead_det_encrypt(message bytea, additional bytea, key_id bigint, context bytea = 'pgsodium', nonce bytea = NULL) 2 | RETURNS bytea 3 | AS 'MODULE_PATHNAME', 'pgsodium_crypto_aead_det_encrypt_by_id' 4 | LANGUAGE c IMMUTABLE; 5 | 6 | CREATE OR REPLACE FUNCTION vault._crypto_aead_det_decrypt(message bytea, additional bytea, key_id bigint, context bytea = 'pgsodium', nonce bytea = NULL) 7 | RETURNS bytea 8 | AS 'MODULE_PATHNAME', 'pgsodium_crypto_aead_det_decrypt_by_id' 9 | LANGUAGE c IMMUTABLE; 10 | 11 | CREATE OR REPLACE FUNCTION vault._crypto_aead_det_noncegen() 12 | RETURNS bytea 13 | AS 'MODULE_PATHNAME', 'pgsodium_crypto_aead_det_noncegen' 14 | LANGUAGE c IMMUTABLE; 15 | 16 | CREATE TABLE vault.secrets ( 17 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 18 | name text, 19 | description text NOT NULL default '', 20 | secret text NOT NULL, 21 | key_id uuid, 22 | nonce bytea DEFAULT vault._crypto_aead_det_noncegen(), 23 | created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, 24 | updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP 25 | ); 26 | 27 | COMMENT ON TABLE vault.secrets IS 'Table with encrypted `secret` column for storing sensitive information on disk.'; 28 | 29 | CREATE UNIQUE INDEX ON vault.secrets USING btree (name) WHERE name IS NOT NULL; 30 | 31 | DROP VIEW IF EXISTS vault.decrypted_secrets; 32 | CREATE VIEW vault.decrypted_secrets AS 33 | SELECT s.id, 34 | s.name, 35 | s.description, 36 | s.secret, 37 | convert_from( 38 | vault._crypto_aead_det_decrypt( 39 | message := decode(s.secret, 'base64'::text), 40 | additional := convert_to(s.id::text, 'utf8'), 41 | key_id := 0, 42 | context := 'pgsodium'::bytea, 43 | nonce := s.nonce 44 | ), 45 | 'utf8'::name 46 | ) AS decrypted_secret, 47 | s.key_id, 48 | s.nonce, 49 | s.created_at, 50 | s.updated_at 51 | FROM vault.secrets s; 52 | 53 | CREATE OR REPLACE FUNCTION vault.create_secret( 54 | new_secret text, 55 | new_name text = NULL, 56 | new_description text = '', 57 | -- unused 58 | new_key_id uuid = NULL 59 | ) 60 | RETURNS uuid 61 | SECURITY DEFINER 62 | LANGUAGE plpgsql 63 | SET search_path = '' 64 | AS $$ 65 | DECLARE 66 | rec record; 67 | BEGIN 68 | INSERT INTO vault.secrets (secret, name, description) 69 | VALUES ( 70 | new_secret, 71 | new_name, 72 | new_description 73 | ) 74 | RETURNING * INTO rec; 75 | UPDATE vault.secrets s 76 | SET secret = encode(vault._crypto_aead_det_encrypt( 77 | message := convert_to(rec.secret, 'utf8'), 78 | additional := convert_to(s.id::text, 'utf8'), 79 | key_id := 0, 80 | context := 'pgsodium'::bytea, 81 | nonce := rec.nonce 82 | ), 'base64') 83 | WHERE id = rec.id; 84 | RETURN rec.id; 85 | END 86 | $$; 87 | 88 | CREATE OR REPLACE FUNCTION vault.update_secret( 89 | secret_id uuid, 90 | new_secret text = NULL, 91 | new_name text = NULL, 92 | new_description text = NULL, 93 | -- unused 94 | new_key_id uuid = NULL 95 | ) 96 | RETURNS void 97 | SECURITY DEFINER 98 | LANGUAGE plpgsql 99 | SET search_path = '' 100 | AS $$ 101 | DECLARE 102 | decrypted_secret text := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE id = secret_id); 103 | BEGIN 104 | UPDATE vault.secrets s 105 | SET 106 | secret = CASE WHEN new_secret IS NULL THEN s.secret 107 | ELSE encode(vault._crypto_aead_det_encrypt( 108 | message := convert_to(new_secret, 'utf8'), 109 | additional := convert_to(s.id::text, 'utf8'), 110 | key_id := 0, 111 | context := 'pgsodium'::bytea, 112 | nonce := s.nonce 113 | ), 'base64') END, 114 | name = coalesce(new_name, s.name), 115 | description = coalesce(new_description, s.description), 116 | updated_at = now() 117 | WHERE s.id = secret_id; 118 | END 119 | $$; 120 | 121 | REVOKE ALL ON FUNCTION 122 | vault._crypto_aead_det_encrypt, 123 | vault._crypto_aead_det_decrypt, 124 | vault._crypto_aead_det_noncegen, 125 | vault.create_secret, 126 | vault.update_secret 127 | FROM PUBLIC; 128 | 129 | SELECT pg_catalog.pg_extension_config_dump('vault.secrets', ''); 130 | -------------------------------------------------------------------------------- /src/crypto_aead_det_xchacha20.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "crypto_aead_det_xchacha20.h" 5 | 6 | static void 7 | s2v_dbl256(unsigned char d[32]) 8 | { 9 | unsigned char t[32]; 10 | unsigned char mask; 11 | size_t i; 12 | 13 | memcpy(t, d, 32); 14 | for (i = 0; i < 32; i++) { 15 | t[i] = (unsigned char) (t[i] << 1); 16 | } 17 | for (i = 31; i != 0; i--) { 18 | t[i - 1] |= d[i] >> 7; 19 | } 20 | mask = ~((d[0] >> 7) - 1); 21 | t[30] ^= (0x04 & mask); 22 | t[31] ^= (0x25 & mask); 23 | memcpy(d, t, 32); 24 | } 25 | 26 | static inline void 27 | s2v_xor(unsigned char *d, const unsigned char *h, size_t len) 28 | { 29 | size_t i; 30 | 31 | for (i = 0; i < len; i++) { 32 | d[i] ^= h[i]; 33 | } 34 | } 35 | 36 | static void 37 | s2v(unsigned char iv[crypto_aead_det_xchacha20_ABYTES], const unsigned char *m, size_t mlen, 38 | const unsigned char *ad, size_t adlen, const unsigned char *nonce, size_t noncelen, 39 | const unsigned char ka[32]) 40 | { 41 | static const unsigned char zero[crypto_aead_det_xchacha20_ABYTES] = { 0 }; 42 | crypto_generichash_state st; 43 | unsigned char d[32]; 44 | 45 | crypto_generichash(d, sizeof d, zero, sizeof zero, ka, sizeof d); 46 | 47 | if (ad != NULL && adlen > 0) { 48 | s2v_dbl256(d); 49 | crypto_generichash(iv, crypto_aead_det_xchacha20_ABYTES, ad, adlen, ka, 32); 50 | s2v_xor(d, iv, sizeof d); 51 | } 52 | if (nonce != NULL && noncelen > 0) { 53 | s2v_dbl256(d); 54 | crypto_generichash(iv, crypto_aead_det_xchacha20_ABYTES, nonce, noncelen, ka, 32); 55 | s2v_xor(d, iv, sizeof d); 56 | } 57 | 58 | crypto_generichash_init(&st, ka, 32, crypto_aead_det_xchacha20_ABYTES); 59 | if (mlen >= crypto_aead_det_xchacha20_ABYTES) { 60 | crypto_generichash_update(&st, m, mlen - crypto_aead_det_xchacha20_ABYTES); 61 | s2v_xor(d, &m[mlen - crypto_aead_det_xchacha20_ABYTES], crypto_aead_det_xchacha20_KEYBYTES); 62 | } else { 63 | s2v_dbl256(d); 64 | s2v_xor(d, m, mlen); 65 | d[mlen] ^= 0x80; 66 | } 67 | crypto_generichash_update(&st, d, sizeof d); 68 | crypto_generichash_final(&st, iv, crypto_aead_det_xchacha20_ABYTES); 69 | } 70 | 71 | int 72 | crypto_aead_det_xchacha20_encrypt_detached( 73 | unsigned char *c, unsigned char mac[crypto_aead_det_xchacha20_ABYTES], const unsigned char *m, 74 | size_t mlen, const unsigned char *ad, size_t adlen, const unsigned char *nonce, 75 | const unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]) 76 | { 77 | unsigned char subkeys[64], *ka = &subkeys[0], *ke = &subkeys[32]; 78 | 79 | crypto_generichash(subkeys, sizeof subkeys, NULL, 0, k, crypto_aead_det_xchacha20_KEYBYTES); 80 | s2v(mac, m, mlen, ad, adlen, nonce, crypto_aead_det_xchacha20_NONCEBYTES, ka); 81 | crypto_stream_xchacha20_xor(c, m, mlen, mac, ke); 82 | 83 | return 0; 84 | } 85 | 86 | int 87 | crypto_aead_det_xchacha20_decrypt_detached( 88 | unsigned char *m, const unsigned char *c, size_t clen, 89 | const unsigned char mac[crypto_aead_det_xchacha20_ABYTES], const unsigned char *ad, 90 | size_t adlen, const unsigned char *nonce, 91 | const unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]) 92 | { 93 | unsigned char subkeys[64], *ka = &subkeys[0], *ke = &subkeys[32]; 94 | unsigned char computed_mac[crypto_aead_det_xchacha20_ABYTES]; 95 | const size_t mlen = clen; 96 | 97 | crypto_generichash(subkeys, sizeof subkeys, NULL, 0, k, crypto_aead_det_xchacha20_KEYBYTES); 98 | crypto_stream_xchacha20_xor(m, c, clen, mac, ke); 99 | s2v(computed_mac, m, mlen, ad, adlen, nonce, crypto_aead_det_xchacha20_NONCEBYTES, ka); 100 | if (sodium_memcmp(mac, computed_mac, crypto_aead_det_xchacha20_ABYTES) != 0) { 101 | memset(m, 0, mlen); 102 | return -1; 103 | } 104 | return 0; 105 | } 106 | 107 | int 108 | crypto_aead_det_xchacha20_encrypt(unsigned char *c, const unsigned char *m, size_t mlen, 109 | const unsigned char *ad, size_t adlen, const unsigned char *nonce, 110 | const unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]) 111 | { 112 | return crypto_aead_det_xchacha20_encrypt_detached(c, c + mlen, m, mlen, ad, adlen, nonce, k); 113 | } 114 | 115 | int 116 | crypto_aead_det_xchacha20_decrypt(unsigned char *m, const unsigned char *c, size_t clen, 117 | const unsigned char *ad, size_t adlen, const unsigned char *nonce, 118 | const unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]) 119 | { 120 | size_t mlen; 121 | 122 | if (clen < crypto_aead_det_xchacha20_ABYTES) { 123 | return -1; 124 | } 125 | mlen = clen - crypto_aead_det_xchacha20_ABYTES; 126 | 127 | return crypto_aead_det_xchacha20_decrypt_detached(m, c, mlen, c + mlen, ad, adlen, nonce, k); 128 | } 129 | 130 | void 131 | crypto_aead_det_xchacha20_keygen(unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]) 132 | { 133 | randombytes_buf(k, crypto_aead_det_xchacha20_KEYBYTES); 134 | } 135 | -------------------------------------------------------------------------------- /src/crypto_aead_det_xchacha20.h: -------------------------------------------------------------------------------- 1 | #ifndef crypto_aead_det_xchacha20_H 2 | #define crypto_aead_det_xchacha20_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | 10 | #define crypto_aead_det_xchacha20_KEYBYTES 32 11 | #define crypto_aead_det_xchacha20_ABYTES 32 12 | #define crypto_aead_det_xchacha20_NONCEBYTES 16 13 | 14 | int crypto_aead_det_xchacha20_encrypt_detached( 15 | unsigned char *c, unsigned char mac[crypto_aead_det_xchacha20_ABYTES], const unsigned char *m, 16 | size_t mlen, const unsigned char *ad, size_t adlen, const unsigned char *nonce, 17 | const unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]); 18 | 19 | int crypto_aead_det_xchacha20_decrypt_detached( 20 | unsigned char *m, const unsigned char *c, size_t clen, 21 | const unsigned char mac[crypto_aead_det_xchacha20_ABYTES], const unsigned char *ad, 22 | size_t adlen, const unsigned char *nonce, 23 | const unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]); 24 | 25 | int crypto_aead_det_xchacha20_encrypt(unsigned char *c, const unsigned char *m, size_t mlen, 26 | const unsigned char *ad, size_t adlen, 27 | const unsigned char *nonce, 28 | const unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]); 29 | 30 | int crypto_aead_det_xchacha20_decrypt(unsigned char *m, const unsigned char *c, size_t clen, 31 | const unsigned char *ad, size_t adlen, 32 | const unsigned char *nonce, 33 | const unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]); 34 | 35 | void crypto_aead_det_xchacha20_keygen(unsigned char k[crypto_aead_det_xchacha20_KEYBYTES]); 36 | 37 | #ifdef __cplusplus 38 | } 39 | #endif 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /src/pgsodium.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "crypto_aead_det_xchacha20.h" 7 | #include "pgsodium.h" 8 | 9 | bytea *pgsodium_secret_key = NULL; 10 | char *getkey_script = NULL; 11 | 12 | PG_FUNCTION_INFO_V1 (pgsodium_crypto_aead_det_encrypt_by_id); 13 | Datum 14 | pgsodium_crypto_aead_det_encrypt_by_id (PG_FUNCTION_ARGS) 15 | { 16 | bytea *message; 17 | bytea *associated; 18 | unsigned long long key_id; 19 | bytea *context; 20 | bytea *key, *nonce; 21 | size_t result_size; 22 | bytea *result; 23 | int success; 24 | 25 | ERRORIF (PG_ARGISNULL (0), "%s: message cannot be NULL"); 26 | ERRORIF (PG_ARGISNULL (2), "%s: key id cannot be NULL"); 27 | ERRORIF (PG_ARGISNULL (3), "%s: key context cannot be NULL"); 28 | 29 | message = PG_GETARG_BYTEA_PP (0); 30 | 31 | if (!PG_ARGISNULL (1)) 32 | { 33 | associated = PG_GETARG_BYTEA_PP (1); 34 | } 35 | else 36 | { 37 | associated = NULL; 38 | } 39 | 40 | key_id = PG_GETARG_INT64 (2); 41 | context = PG_GETARG_BYTEA_PP (3); 42 | 43 | if (!PG_ARGISNULL (4)) 44 | { 45 | nonce = PG_GETARG_BYTEA_PP (4); 46 | ERRORIF (VARSIZE_ANY_EXHDR (nonce) != 47 | crypto_aead_det_xchacha20_NONCEBYTES, "%s: invalid nonce"); 48 | } 49 | else 50 | { 51 | nonce = NULL; 52 | } 53 | 54 | result_size = 55 | VARSIZE_ANY_EXHDR (message) + crypto_aead_det_xchacha20_ABYTES; 56 | result = _pgsodium_zalloc_bytea (result_size + VARHDRSZ); 57 | 58 | key = 59 | pgsodium_derive_helper (key_id, crypto_aead_det_xchacha20_KEYBYTES, 60 | context); 61 | 62 | success = crypto_aead_det_xchacha20_encrypt ( 63 | PGSODIUM_UCHARDATA (result), 64 | PGSODIUM_UCHARDATA_ANY (message), 65 | VARSIZE_ANY_EXHDR (message), 66 | associated != NULL ? PGSODIUM_UCHARDATA_ANY (associated) : NULL, 67 | associated != NULL ? VARSIZE_ANY_EXHDR (associated) : 0, 68 | nonce != NULL ? PGSODIUM_UCHARDATA_ANY (nonce) : NULL, 69 | PGSODIUM_UCHARDATA_ANY (key)); 70 | ERRORIF (success != 0, "%s: failed"); 71 | PG_RETURN_BYTEA_P (result); 72 | } 73 | 74 | PG_FUNCTION_INFO_V1 (pgsodium_crypto_aead_det_decrypt_by_id); 75 | Datum 76 | pgsodium_crypto_aead_det_decrypt_by_id (PG_FUNCTION_ARGS) 77 | { 78 | bytea *ciphertext; 79 | bytea *associated; 80 | unsigned long long key_id; 81 | bytea *context; 82 | size_t result_len; 83 | bytea *key, *result, *nonce; 84 | int success; 85 | 86 | 87 | ERRORIF (PG_ARGISNULL (0), "%s: message cannot be NULL"); 88 | ERRORIF (PG_ARGISNULL (2), "%s: key id cannot be NULL"); 89 | ERRORIF (PG_ARGISNULL (3), "%s: key context cannot be NULL"); 90 | 91 | ciphertext = PG_GETARG_BYTEA_PP (0); 92 | 93 | if (!PG_ARGISNULL (1)) 94 | { 95 | associated = PG_GETARG_BYTEA_PP (1); 96 | } 97 | else 98 | { 99 | associated = NULL; 100 | } 101 | 102 | key_id = PG_GETARG_INT64 (2); 103 | context = PG_GETARG_BYTEA_PP (3); 104 | 105 | if (!PG_ARGISNULL (4)) 106 | { 107 | nonce = PG_GETARG_BYTEA_PP (4); 108 | ERRORIF (VARSIZE_ANY_EXHDR (nonce) != 109 | crypto_aead_det_xchacha20_NONCEBYTES, "%s: invalid nonce"); 110 | } 111 | else 112 | { 113 | nonce = NULL; 114 | } 115 | ERRORIF (VARSIZE_ANY_EXHDR (ciphertext) < 116 | crypto_aead_det_xchacha20_ABYTES, "%s: invalid message"); 117 | result_len = 118 | VARSIZE_ANY_EXHDR (ciphertext) - crypto_aead_det_xchacha20_ABYTES; 119 | result = _pgsodium_zalloc_bytea (result_len + VARHDRSZ); 120 | key = 121 | pgsodium_derive_helper (key_id, crypto_aead_det_xchacha20_KEYBYTES, 122 | context); 123 | 124 | success = crypto_aead_det_xchacha20_decrypt ( 125 | PGSODIUM_UCHARDATA (result), 126 | PGSODIUM_UCHARDATA_ANY (ciphertext), 127 | VARSIZE_ANY_EXHDR (ciphertext), 128 | associated != NULL ? PGSODIUM_UCHARDATA_ANY (associated) : NULL, 129 | associated != NULL ? VARSIZE_ANY_EXHDR (associated) : 0, 130 | nonce != NULL ? PGSODIUM_UCHARDATA_ANY (nonce) : NULL, 131 | PGSODIUM_UCHARDATA_ANY (key)); 132 | ERRORIF (success != 0, "%s: invalid ciphertext"); 133 | PG_RETURN_BYTEA_P (result); 134 | } 135 | 136 | PG_FUNCTION_INFO_V1 (pgsodium_crypto_aead_det_noncegen); 137 | Datum 138 | pgsodium_crypto_aead_det_noncegen (PG_FUNCTION_ARGS) 139 | { 140 | int result_size = VARHDRSZ + crypto_aead_det_xchacha20_NONCEBYTES; 141 | bytea *result = _pgsodium_zalloc_bytea (result_size); 142 | randombytes_buf (VARDATA (result), crypto_aead_det_xchacha20_NONCEBYTES); 143 | PG_RETURN_BYTEA_P (result); 144 | } 145 | -------------------------------------------------------------------------------- /src/pgsodium.h: -------------------------------------------------------------------------------- 1 | #ifndef PGSODIUM_H 2 | #define PGSODIUM_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #if PG_VERSION_NUM >= 160000 13 | #include 14 | #endif 15 | 16 | #define elogn(s) elog(NOTICE, "%s", (s)) 17 | #define elogn1(s, v) elog(NOTICE, "%s: %lu", (s), (v)) 18 | 19 | #define PG_GETKEY_EXEC "pgsodium_getkey" 20 | 21 | #define PGSODIUM_UCHARDATA(_vlena) (unsigned char *)VARDATA(_vlena) 22 | #define PGSODIUM_CHARDATA(_vlena) (char *)VARDATA(_vlena) 23 | 24 | #define PGSODIUM_UCHARDATA_ANY(_vlena) (unsigned char *)VARDATA_ANY(_vlena) 25 | #define PGSODIUM_CHARDATA_ANY(_vlena) (char *)VARDATA_ANY(_vlena) 26 | 27 | #define ERRORIF(B, msg) \ 28 | if ((B)) \ 29 | ereport(ERROR, (errcode(ERRCODE_DATA_EXCEPTION), errmsg(msg, __func__))) 30 | 31 | typedef struct _pgsodium_cb 32 | { 33 | void *ptr; 34 | size_t size; 35 | } _pgsodium_cb; 36 | 37 | static void context_cb_zero_buff (void *); 38 | 39 | static void 40 | context_cb_zero_buff (void *a) 41 | { 42 | _pgsodium_cb *data = (_pgsodium_cb *) a; 43 | sodium_memzero (data->ptr, data->size); 44 | } 45 | 46 | static inline bytea *_pgsodium_zalloc_bytea (size_t); 47 | static inline bytea *pgsodium_derive_helper (unsigned long long subkey_id, 48 | size_t subkey_size, bytea * context); 49 | 50 | extern bytea *pgsodium_secret_key; 51 | extern char *getkey_script; 52 | 53 | /* allocator attached zero-callback to clean up memory */ 54 | static inline bytea * 55 | _pgsodium_zalloc_bytea (size_t allocation_size) 56 | { 57 | bytea *result = (bytea *) palloc (allocation_size); 58 | MemoryContextCallback *ctxcb = 59 | (MemoryContextCallback *) MemoryContextAlloc (CurrentMemoryContext, 60 | sizeof (MemoryContextCallback)); 61 | _pgsodium_cb *d = (_pgsodium_cb *) palloc (sizeof (_pgsodium_cb)); 62 | d->ptr = result; 63 | d->size = allocation_size; 64 | ctxcb->func = context_cb_zero_buff; 65 | ctxcb->arg = d; 66 | MemoryContextRegisterResetCallback (CurrentMemoryContext, ctxcb); // verify where this cb fires 67 | SET_VARSIZE (result, allocation_size); 68 | return result; 69 | } 70 | 71 | static inline text * 72 | _pgsodium_zalloc_text (size_t allocation_size) 73 | { 74 | text *result = (text *) palloc (allocation_size); 75 | MemoryContextCallback *ctxcb = 76 | (MemoryContextCallback *) MemoryContextAlloc (CurrentMemoryContext, 77 | sizeof (MemoryContextCallback)); 78 | _pgsodium_cb *d = (_pgsodium_cb *) palloc (sizeof (_pgsodium_cb)); 79 | d->ptr = result; 80 | d->size = allocation_size; 81 | ctxcb->func = context_cb_zero_buff; 82 | ctxcb->arg = d; 83 | MemoryContextRegisterResetCallback (CurrentMemoryContext, ctxcb); 84 | SET_VARSIZE (result, allocation_size); 85 | return result; 86 | } 87 | 88 | static inline bytea * 89 | pgsodium_derive_helper (unsigned long long subkey_id, 90 | size_t subkey_size, bytea * context) 91 | { 92 | size_t result_size; 93 | bytea *result; 94 | ERRORIF (pgsodium_secret_key == NULL, 95 | "%s: pgsodium_derive: no server secret key defined."); 96 | ERRORIF (subkey_size < crypto_kdf_BYTES_MIN || 97 | subkey_size > crypto_kdf_BYTES_MAX, 98 | "%s: crypto_kdf_derive_from_key: invalid key size requested"); 99 | ERRORIF (VARSIZE_ANY_EXHDR (context) != 8, 100 | "%s: crypto_kdf_derive_from_key: context must be 8 bytes"); 101 | result_size = VARHDRSZ + subkey_size; 102 | result = _pgsodium_zalloc_bytea (result_size); 103 | crypto_kdf_derive_from_key (PGSODIUM_UCHARDATA (result), 104 | subkey_size, 105 | subkey_id, 106 | (const char *) VARDATA_ANY (context), 107 | PGSODIUM_UCHARDATA (pgsodium_secret_key)); 108 | return result; 109 | } 110 | 111 | #endif /* PGSODIUM_H */ 112 | -------------------------------------------------------------------------------- /src/supabase_vault.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "pgsodium.h" 11 | 12 | PG_MODULE_MAGIC; 13 | 14 | void _PG_init(void); 15 | 16 | void 17 | _PG_init (void) 18 | { 19 | FILE *fp; 20 | char *secret_buf = NULL; 21 | size_t secret_len = 0; 22 | size_t char_read; 23 | char *path; 24 | char sharepath[MAXPGPATH]; 25 | 26 | if (sodium_init () == -1) 27 | { 28 | elog (ERROR, 29 | "_PG_init: sodium_init() failed cannot initialize supabase_vault"); 30 | return; 31 | } 32 | 33 | // we're done if not preloaded, otherwise try to get internal shared key 34 | if (!process_shared_preload_libraries_in_progress) 35 | return; 36 | 37 | path = (char *) palloc0 (MAXPGPATH); 38 | get_share_path (my_exec_path, sharepath); 39 | snprintf (path, MAXPGPATH, "%s/extension/%s", sharepath, PG_GETKEY_EXEC); 40 | 41 | DefineCustomStringVariable ("vault.getkey_script", 42 | "path to script that returns vault root key", 43 | NULL, &getkey_script, path, PGC_POSTMASTER, 0, NULL, NULL, NULL); 44 | 45 | if (access (getkey_script, X_OK) == -1) 46 | { 47 | if (errno == ENOENT) 48 | ereport(ERROR, ( 49 | errmsg("The getkey script \"%s\" does not exist.", getkey_script), 50 | errdetail("The getkey script fetches the primary server secret key."), 51 | errhint("You might want to create it and/or set \"vault.getkey_script\" to the correct path."))); 52 | else if (errno == EACCES) 53 | ereport(ERROR, 54 | errmsg("Permission denied for the getkey script \"%s\"", 55 | getkey_script)); 56 | else 57 | ereport(ERROR, 58 | errmsg("Can not access getkey script \"%s\"", getkey_script)); 59 | proc_exit (1); 60 | } 61 | 62 | if ((fp = popen (getkey_script, "r")) == NULL) 63 | { 64 | ereport(ERROR, 65 | errmsg("%s: could not launch shell command from", getkey_script)); 66 | proc_exit (1); 67 | } 68 | 69 | char_read = getline (&secret_buf, &secret_len, fp); 70 | if (secret_buf[char_read - 1] == '\n') 71 | secret_buf[char_read - 1] = '\0'; 72 | 73 | secret_len = strlen (secret_buf); 74 | 75 | if (secret_len != 64) 76 | { 77 | ereport(ERROR, errmsg("invalid secret key")); 78 | proc_exit (1); 79 | } 80 | 81 | if (pclose (fp) != 0) 82 | { 83 | ereport(ERROR, errmsg( "%s: could not close shell command\n", 84 | PG_GETKEY_EXEC)); 85 | proc_exit (1); 86 | } 87 | pgsodium_secret_key = 88 | sodium_malloc (crypto_sign_SECRETKEYBYTES + VARHDRSZ); 89 | 90 | if (pgsodium_secret_key == NULL) 91 | { 92 | ereport(ERROR, errmsg( "%s: sodium_malloc() failed\n", PG_GETKEY_EXEC)); 93 | proc_exit (1); 94 | } 95 | 96 | hex_decode (secret_buf, secret_len, VARDATA (pgsodium_secret_key)); 97 | sodium_memzero (secret_buf, secret_len); 98 | free (secret_buf); 99 | elog (LOG, "vault primary server secret key loaded"); 100 | } 101 | -------------------------------------------------------------------------------- /supabase_vault--0.2.8.control: -------------------------------------------------------------------------------- 1 | requires = pgsodium 2 | -------------------------------------------------------------------------------- /supabase_vault.control.in: -------------------------------------------------------------------------------- 1 | comment = 'Supabase Vault Extension' 2 | default_version = '@VAULT_VERSION@' 3 | module_pathname = '$libdir/supabase_vault' 4 | relocatable = false 5 | schema = vault 6 | -------------------------------------------------------------------------------- /test/expected/test.out: -------------------------------------------------------------------------------- 1 | select no_plan(); 2 | no_plan 3 | --------- 4 | (0 rows) 5 | 6 | do $$ 7 | begin 8 | perform vault.create_secret('s3kr3t_k3y', 'a_name', 'this is the foo secret key'); 9 | end 10 | $$; 11 | SELECT results_eq( 12 | $$ 13 | SELECT decrypted_secret = 's3kr3t_k3y', description = 'this is the foo secret key' 14 | FROM vault.decrypted_secrets WHERE name = 'a_name'; 15 | $$, 16 | $$VALUES (true, true)$$, 17 | 'can select from masking view with custom key'); 18 | results_eq 19 | ----------------------------------------------------- 20 | ok 1 - can select from masking view with custom key 21 | (1 row) 22 | 23 | SELECT lives_ok( 24 | $test$ 25 | select vault.update_secret( 26 | (select id from vault.secrets where name = 'a_name'), new_name:='a_new_name', 27 | new_secret:='new_s3kr3t_k3y', new_description:='this is the bar key') 28 | $test$, 29 | 'can update name, secret and description' 30 | ); 31 | lives_ok 32 | ------------------------------------------------ 33 | ok 2 - can update name, secret and description 34 | (1 row) 35 | 36 | TRUNCATE vault.secrets; 37 | set role bob; 38 | do $$ 39 | begin 40 | perform vault.create_secret ('foo', 'bar', 'baz'); 41 | end 42 | $$; 43 | select results_eq( 44 | $test$ 45 | SELECT (decrypted_secret COLLATE "default"), name, description FROM vault.decrypted_secrets 46 | WHERE name = 'bar' 47 | $test$, 48 | $results$values ('foo', 'bar', 'baz')$results$, 49 | 'bob can query a secret'); 50 | results_eq 51 | ------------------------------- 52 | ok 3 - bob can query a secret 53 | (1 row) 54 | 55 | select lives_ok( 56 | $test$ 57 | select vault.update_secret( 58 | (select id from vault.secrets where name = 'bar'), 59 | 'fooz', 60 | 'barz', 61 | 'bazz') 62 | $test$, 63 | 'bob can update a secret'); 64 | lives_ok 65 | -------------------------------- 66 | ok 4 - bob can update a secret 67 | (1 row) 68 | 69 | select results_eq( 70 | $test$ 71 | SELECT (decrypted_secret COLLATE "default"), name, description 72 | FROM vault.decrypted_secrets 73 | $test$, 74 | $results$values ('fooz', 'barz', 'bazz')$results$, 75 | 'bob can query an updated secret'); 76 | results_eq 77 | ---------------------------------------- 78 | ok 5 - bob can query an updated secret 79 | (1 row) 80 | 81 | reset role; 82 | truncate vault.secrets; 83 | do $$ 84 | begin 85 | perform vault.create_secret( 86 | new_secret := '', 87 | new_name := 'empty_secret' 88 | ); 89 | end 90 | $$; 91 | select results_eq( 92 | $test$ 93 | select decrypted_secret collate "default" 94 | from vault.decrypted_secrets 95 | where name = 'empty_secret' 96 | $test$, 97 | $results$values ('')$results$, 98 | 'secret can be an empty string' 99 | ); 100 | results_eq 101 | -------------------------------------- 102 | ok 6 - secret can be an empty string 103 | (1 row) 104 | 105 | select * from finish(); 106 | finish 107 | -------- 108 | 1..6 109 | (1 row) 110 | 111 | -------------------------------------------------------------------------------- /test/fixtures.sql: -------------------------------------------------------------------------------- 1 | CREATE ROLE bob login password 'bob'; 2 | 3 | CREATE EXTENSION IF NOT EXISTS pgtap; 4 | CREATE EXTENSION supabase_vault CASCADE; 5 | 6 | GRANT USAGE ON SCHEMA vault TO bob WITH GRANT OPTION; 7 | GRANT SELECT ON vault.secrets, vault.decrypted_secrets TO bob WITH GRANT OPTION; 8 | GRANT EXECUTE ON FUNCTION 9 | vault.create_secret, 10 | vault.update_secret, 11 | vault._crypto_aead_det_decrypt 12 | TO bob WITH GRANT OPTION; 13 | -------------------------------------------------------------------------------- /test/sql/test.sql: -------------------------------------------------------------------------------- 1 | select no_plan(); 2 | 3 | do $$ 4 | begin 5 | perform vault.create_secret('s3kr3t_k3y', 'a_name', 'this is the foo secret key'); 6 | end 7 | $$; 8 | 9 | SELECT results_eq( 10 | $$ 11 | SELECT decrypted_secret = 's3kr3t_k3y', description = 'this is the foo secret key' 12 | FROM vault.decrypted_secrets WHERE name = 'a_name'; 13 | $$, 14 | $$VALUES (true, true)$$, 15 | 'can select from masking view with custom key'); 16 | 17 | SELECT lives_ok( 18 | $test$ 19 | select vault.update_secret( 20 | (select id from vault.secrets where name = 'a_name'), new_name:='a_new_name', 21 | new_secret:='new_s3kr3t_k3y', new_description:='this is the bar key') 22 | $test$, 23 | 'can update name, secret and description' 24 | ); 25 | 26 | TRUNCATE vault.secrets; 27 | 28 | set role bob; 29 | 30 | do $$ 31 | begin 32 | perform vault.create_secret ('foo', 'bar', 'baz'); 33 | end 34 | $$; 35 | 36 | select results_eq( 37 | $test$ 38 | SELECT (decrypted_secret COLLATE "default"), name, description FROM vault.decrypted_secrets 39 | WHERE name = 'bar' 40 | $test$, 41 | $results$values ('foo', 'bar', 'baz')$results$, 42 | 'bob can query a secret'); 43 | 44 | select lives_ok( 45 | $test$ 46 | select vault.update_secret( 47 | (select id from vault.secrets where name = 'bar'), 48 | 'fooz', 49 | 'barz', 50 | 'bazz') 51 | $test$, 52 | 'bob can update a secret'); 53 | 54 | select results_eq( 55 | $test$ 56 | SELECT (decrypted_secret COLLATE "default"), name, description 57 | FROM vault.decrypted_secrets 58 | $test$, 59 | $results$values ('fooz', 'barz', 'bazz')$results$, 60 | 'bob can query an updated secret'); 61 | 62 | reset role; 63 | truncate vault.secrets; 64 | 65 | do $$ 66 | begin 67 | perform vault.create_secret( 68 | new_secret := '', 69 | new_name := 'empty_secret' 70 | ); 71 | end 72 | $$; 73 | 74 | select results_eq( 75 | $test$ 76 | select decrypted_secret collate "default" 77 | from vault.decrypted_secrets 78 | where name = 'empty_secret' 79 | $test$, 80 | $results$values ('')$results$, 81 | 'secret can be an empty string' 82 | ); 83 | 84 | select * from finish(); 85 | --------------------------------------------------------------------------------