Implement OpenCreds: two more item types, and a portable vault - #22
Merged
Conversation
The vault could hold logins, cards, identities and notes, and could only leave through a CSV. A CSV is plaintext by construction, drops whatever it has no column for -- password history, TOTP seeds, folders, custom fields, URI match rules -- and carries no integrity: one truncated at 3,000 rows imports 3,000 rows and reports success. This implements OpenCreds 0.1 (https://logicsrc.com/opencreds), which specifies the record, the envelope and a portable file. Two more item types, both things people already keep in a vault and neither expressible as a login: - key (5): SSH and PGP keys, API tokens, certificates, .env secrets. Carries path and mode, so a restore is total -- a private key written back 0644 is a key ssh refuses to use, and one at the wrong path is a key nothing finds. - account (6): a provider account and the OAuth tokens that act as it. Deliberately not a login: a login is what a person types at a sign-in form, an account is what a machine presents to an API. Conflating them is how a rotated refresh token ends up in a password history array. Codes 1-4 are untouched. This vault's deployed codes are what fixed them in the specification; renumbering would break every ciphertext already written. For the same reason the vault declares the namespace `marksyncr` rather than adopting `opencreds` -- its labels are compiled into the AAD of every ciphertext it has written, so editing one would not migrate a vault, it would make it undecryptable. `marksyncr` is a registered namespace, and not one existing vault needed re-encrypting. The portable database, both directions. Encrypted by default under a key derived from an export passphrase, not the vault's user key -- a file encrypted under the user key only opens inside the vault it came from. The header is bound as AAD over the payload, so the manifest is authenticated by the same tag as the data: the item count is previewable before anyone types a passphrase, and cannot be a lie. Import writes nothing when the passphrase is wrong, the file was altered, or the manifest disagrees with the payload. Import accepts an .opencreds file alongside the CSVs, prompting for the passphrase with the real item count in the prompt. The editor gains fields for both new types, and vault_items accepts type 5 and 6 in the database and in the API validator. packages/vault/__tests__/opencreds-interop.test.js drives this vault against the reference implementation in both directions and asserts the item records come back byte-identical -- a round trip inside one implementation only proves the code agrees with itself. It skips when the reference is not installed rather than reddening CI; point OPENCREDS_REF at a built checkout to run it. Verified passing against @logicsrc/opencreds: six types out and back, history, key modes and account scopes intact, wrong passphrases and restated manifests refused. 149 vault tests, 786 extension tests, lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRQrfuwuYKKV5UB9kLHuX5
| }), | ||
| impl.createItem('account', { | ||
| name: 'Stripe', | ||
| account: { provider: 'stripe', accessToken: 'sk_live_x', scopes: ['charges:write', 'customers:read'] }, |
| }), | ||
| createItem('account', { | ||
| name: 'Stripe', | ||
| account: { provider: 'stripe', accessToken: 'sk_live_x', scopes: ['charges:write'] }, |
| const userKey = randomBytes(32); | ||
| for (const item of [ | ||
| createItem('key', { name: 'deploy', key: { keyType: 'ssh', privateKey: 'SECRET' } }), | ||
| createItem('account', { name: 'Stripe', account: { accessToken: 'sk_live_x' } }), |
ThreatCrush Security Scan15 finding(s) MEDIUM: 8 | LOW: 7
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The vault could hold logins, cards, identities and notes, and could only leave through a CSV. A CSV is plaintext by construction, drops whatever it has no column for — password history, TOTP seeds, folders, custom fields, URI match rules — and carries no integrity: one truncated at 3,000 rows imports 3,000 rows and reports success.
This implements OpenCreds 0.1 (https://logicsrc.com/opencreds), which specifies the record, the envelope and a portable file. Spec PR: profullstack/logicsrc#140.
Two more item types
Both are things people already keep in a vault, and neither is expressible as a login:
key(5) — SSH and PGP keys, API tokens, certificates, .env secrets. Carriespathandmode, so a restore is total: a private key written back0644is a keysshrefuses to use, and one at the wrong path is a key nothing finds.account(6) — a provider account and the OAuth tokens that act as it. Deliberately not a login: a login is what a person types at a sign-in form, an account is what a machine presents to an API. They expire differently and are revoked differently, and conflating them is how a rotated refresh token ends up in a password history array.Codes 1–4 are untouched. This vault's deployed codes are what fixed them in the specification; renumbering would break every ciphertext already written. For the same reason the vault declares the namespace
marksyncrrather than adoptingopencreds— its labels are compiled into the AAD of every ciphertext it has written, so editing one would not migrate a vault, it would make it undecryptable.marksyncris a registered namespace, and not one existing vault needs re-encrypting.The portable database, both directions
Encrypted by default under a key derived from an export passphrase, not the vault's user key — a file encrypted under the user key only opens inside the vault it came from, which is the opposite of portable.
The header is bound as AAD over the payload, so the manifest is authenticated by the same tag as the data: the item count is previewable before anyone types a passphrase, and cannot be a lie. Import writes nothing when the passphrase is wrong, the file was altered, or the manifest disagrees with the payload.
UI
Import accepts an
.opencredsfile alongside the CSVs, prompting for the passphrase with the real item count in the prompt. The editor gains fields for both new types, andvault_itemsaccepts type 5 and 6 in the database (migration) and in the API validator.Verification
packages/vault/__tests__/opencreds-interop.test.jsdrives this vault against the reference implementation in both directions and asserts the item records come back byte-identical — a round trip inside one implementation only proves the code agrees with itself. Verified passing against@logicsrc/opencreds: six types out and back, password history, key modes and account scopes intact; wrong passphrases and restated manifests refused.It skips when the reference is not installed rather than reddening CI. Point
OPENCREDS_REFat a built checkout to run it.extension-authfailures are pre-existing on master and unrelatedvault-validation.test.jsasserted type 5 was rejected, which is precisely what this widens🤖 Generated with Claude Code
https://claude.ai/code/session_01QRQrfuwuYKKV5UB9kLHuX5