@kubun/credential 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +57 -0
- package/lib/authority.d.ts +29 -0
- package/lib/authority.js +26 -0
- package/lib/builder.d.ts +82 -0
- package/lib/builder.js +76 -0
- package/lib/digest.d.ts +53 -0
- package/lib/digest.js +82 -0
- package/lib/errors.d.ts +169 -0
- package/lib/errors.js +248 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +10 -0
- package/lib/key-cache.d.ts +34 -0
- package/lib/key-cache.js +55 -0
- package/lib/manager.d.ts +280 -0
- package/lib/manager.js +881 -0
- package/lib/op.d.ts +128 -0
- package/lib/op.js +304 -0
- package/lib/pin-throttle.d.ts +44 -0
- package/lib/pin-throttle.js +48 -0
- package/lib/provenance.d.ts +6 -0
- package/lib/provenance.js +8 -0
- package/lib/suite.d.ts +112 -0
- package/lib/suite.js +317 -0
- package/lib/vocabulary.d.ts +16 -0
- package/lib/vocabulary.js +17 -0
- package/package.json +45 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# The Prosperity Public License 3.0.0
|
|
2
|
+
|
|
3
|
+
Contributor: Paul Le Cam
|
|
4
|
+
|
|
5
|
+
Source Code: https://github.com/PaulLeCam/kubun
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
This license allows you to use and share this software for noncommercial purposes for free and to try this software for commercial purposes for thirty days.
|
|
10
|
+
|
|
11
|
+
## Agreement
|
|
12
|
+
|
|
13
|
+
In order to receive this license, you have to agree to its rules. Those rules are both obligations under that agreement and conditions to your license. Don't do anything with this software that triggers a rule you can't or won't follow.
|
|
14
|
+
|
|
15
|
+
## Notices
|
|
16
|
+
|
|
17
|
+
Make sure everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license and the contributor and source code lines above.
|
|
18
|
+
|
|
19
|
+
## Commercial Trial
|
|
20
|
+
|
|
21
|
+
Limit your use of this software for commercial purposes to a thirty-day trial period. If you use this software for work, your company gets one trial period for all personnel, not one trial per person.
|
|
22
|
+
|
|
23
|
+
## Contributions Back
|
|
24
|
+
|
|
25
|
+
Developing feedback, changes, or additions that you contribute back to the contributor on the terms of a standardized public software license such as [the Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0), [the Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html), [the MIT license](https://spdx.org/licenses/MIT.html), or [the two-clause BSD license](https://spdx.org/licenses/BSD-2-Clause.html) doesn't count as use for a commercial purpose.
|
|
26
|
+
|
|
27
|
+
## Personal Uses
|
|
28
|
+
|
|
29
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, doesn't count as use for a commercial purpose.
|
|
30
|
+
|
|
31
|
+
## Noncommercial Organizations
|
|
32
|
+
|
|
33
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution doesn't count as use for a commercial purpose regardless of the source of funding or obligations resulting from the funding.
|
|
34
|
+
|
|
35
|
+
## Defense
|
|
36
|
+
|
|
37
|
+
Don't make any legal claim against anyone accusing this software, with or without changes, alone or with other technology, of infringing any patent.
|
|
38
|
+
|
|
39
|
+
## Copyright
|
|
40
|
+
|
|
41
|
+
The contributor licenses you to do everything with this software that would otherwise infringe their copyright in it.
|
|
42
|
+
|
|
43
|
+
## Patent
|
|
44
|
+
|
|
45
|
+
The contributor licenses you to do everything with this software that would otherwise infringe any patents they can license or become able to license.
|
|
46
|
+
|
|
47
|
+
## Reliability
|
|
48
|
+
|
|
49
|
+
The contributor can't revoke this license.
|
|
50
|
+
|
|
51
|
+
## Excuse
|
|
52
|
+
|
|
53
|
+
You're excused for unknowingly breaking [Notices](#notices) if you take all practical steps to comply within thirty days of learning you broke the rule.
|
|
54
|
+
|
|
55
|
+
## No Liability
|
|
56
|
+
|
|
57
|
+
***As far as the law allows, this software comes as is, without any warranty or condition, and the contributor won't be liable to anyone for any damages related to this software or this license, under any kind of legal claim.***
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CredentialKey, CredentialStoreAPI } from '@kubun/store-credential';
|
|
2
|
+
/**
|
|
3
|
+
* Two authority levels, distinct only for a non-owner: a recipient already holds a
|
|
4
|
+
* wrapping that decrypts, so `read` (serve the ciphertext) is free to grant, while
|
|
5
|
+
* `administer` (grant/rotate/revoke) by a non-owner would make a key un-revokable.
|
|
6
|
+
*/
|
|
7
|
+
export type CredentialAuthorization = 'read' | 'administer';
|
|
8
|
+
export type CredentialAuthorityParams = {
|
|
9
|
+
key: CredentialKey;
|
|
10
|
+
/** The request's store, so an answer may consult rows the key alone does not carry. */
|
|
11
|
+
store: CredentialStoreAPI;
|
|
12
|
+
};
|
|
13
|
+
export type CredentialAuthority = {
|
|
14
|
+
/**
|
|
15
|
+
* The DID a key minted by this viewer belongs to.
|
|
16
|
+
*
|
|
17
|
+
* Asked rather than assumed because under delegation the owner is neither the
|
|
18
|
+
* client's input nor the signer, but what the chain resolves to.
|
|
19
|
+
*/
|
|
20
|
+
ownerForNewKey(): Promise<string>;
|
|
21
|
+
authorize(authorization: CredentialAuthorization, params: CredentialAuthorityParams): Promise<boolean>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The stand-in until ownership is a controller/agent capability chain: the viewer
|
|
25
|
+
* is one device and owns what it minted. `owner_did` is normalized (compare
|
|
26
|
+
* normalized, or a `did:peer:4` long-form viewer is refused its own vault);
|
|
27
|
+
* `recipient_did` is a key-resolution input, never normalized, so it is folded here.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createDeviceAuthority(rawViewerDID: string): CredentialAuthority;
|
package/lib/authority.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
/**
|
|
3
|
+
* The stand-in until ownership is a controller/agent capability chain: the viewer
|
|
4
|
+
* is one device and owns what it minted. `owner_did` is normalized (compare
|
|
5
|
+
* normalized, or a `did:peer:4` long-form viewer is refused its own vault);
|
|
6
|
+
* `recipient_did` is a key-resolution input, never normalized, so it is folded here.
|
|
7
|
+
*/ export function createDeviceAuthority(rawViewerDID) {
|
|
8
|
+
const viewerDID = normalizeDID(rawViewerDID);
|
|
9
|
+
return {
|
|
10
|
+
async ownerForNewKey () {
|
|
11
|
+
return viewerDID;
|
|
12
|
+
},
|
|
13
|
+
async authorize (authorization, { key, store }) {
|
|
14
|
+
if (normalizeDID(key.owner_did) === viewerDID) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (authorization === 'administer') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
// Current version only: a recipient left behind by a rotation or revocation
|
|
21
|
+
// must read as "not a recipient", not as history.
|
|
22
|
+
const wrappings = await store.listWrappings(key.key_id, key.version);
|
|
23
|
+
return wrappings.some((wrapping)=>wrapping.recipient_did != null && normalizeDID(wrapping.recipient_did) === viewerDID);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
package/lib/builder.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type MethodRegistry } from '@kokuin/token';
|
|
2
|
+
import type { CredentialFactorDescriptor } from '@kubun/store-credential';
|
|
3
|
+
import type { Runtime } from '@sozai/runtime';
|
|
4
|
+
import { type FactorInput } from './suite.js';
|
|
5
|
+
/**
|
|
6
|
+
* Credential material built as values, with no store and no private key.
|
|
7
|
+
*
|
|
8
|
+
* The party that runs an OAuth flow is the only one holding the token in
|
|
9
|
+
* plaintext, and it is frequently a client with no credential database of its
|
|
10
|
+
* own. It can still produce everything the server needs to persist: sender-side
|
|
11
|
+
* ECDH-ES consumes only the recipients' *published* agreement keys, so a client
|
|
12
|
+
* wraps a content key to a server's DID without that server participating and
|
|
13
|
+
* without holding any private key itself.
|
|
14
|
+
*
|
|
15
|
+
* Nothing in this module imports `CredentialStoreAPI`, and a test builds a key
|
|
16
|
+
* with no store in scope to keep that true. A builder that quietly grew a store
|
|
17
|
+
* dependency would compile and would be unusable by the caller it exists for.
|
|
18
|
+
*/
|
|
19
|
+
/** Enough to encrypt an entry under a key. */
|
|
20
|
+
export type CredentialKeyMaterial = {
|
|
21
|
+
keyID: string;
|
|
22
|
+
ownerDID: string;
|
|
23
|
+
suite: number;
|
|
24
|
+
version: number;
|
|
25
|
+
/**
|
|
26
|
+
* Never transmitted. The wrappings are how this reaches a recipient; sending
|
|
27
|
+
* it beside them would make every wrapping decorative.
|
|
28
|
+
*/
|
|
29
|
+
contentKey: Uint8Array;
|
|
30
|
+
};
|
|
31
|
+
export type BuiltWrapping = {
|
|
32
|
+
wrappingID: string;
|
|
33
|
+
keyID: string;
|
|
34
|
+
keyVersion: number;
|
|
35
|
+
factors: Array<CredentialFactorDescriptor>;
|
|
36
|
+
iv: Uint8Array;
|
|
37
|
+
wrappedKey: Uint8Array;
|
|
38
|
+
recipientDID: string | null;
|
|
39
|
+
};
|
|
40
|
+
export type BuiltKey = CredentialKeyMaterial & {
|
|
41
|
+
wrappings: Array<BuiltWrapping>;
|
|
42
|
+
};
|
|
43
|
+
export type BuildKeyParams = {
|
|
44
|
+
runtime: Runtime;
|
|
45
|
+
/**
|
|
46
|
+
* Who may later grant, rotate and revoke — not necessarily anyone who can
|
|
47
|
+
* decrypt. On a peer this is also the sole recipient; on a multi-tenant server
|
|
48
|
+
* the owner is the viewer and the server is a recipient.
|
|
49
|
+
*/
|
|
50
|
+
ownerDID: string;
|
|
51
|
+
suite?: number;
|
|
52
|
+
/**
|
|
53
|
+
* One inner array per wrapping, holding the factors that wrapping ANDs
|
|
54
|
+
* together. The outer array is the OR: any one wrapping opens the key.
|
|
55
|
+
*/
|
|
56
|
+
wrappings: Array<Array<FactorInput>>;
|
|
57
|
+
/** Resolvers for recipient DIDs that carry no key material of their own (`did:kokuin:`). */
|
|
58
|
+
methods?: MethodRegistry;
|
|
59
|
+
};
|
|
60
|
+
export type BuiltEntry = {
|
|
61
|
+
entryID: string;
|
|
62
|
+
keyID: string;
|
|
63
|
+
keyVersion: number;
|
|
64
|
+
iv: Uint8Array;
|
|
65
|
+
ciphertext: Uint8Array;
|
|
66
|
+
};
|
|
67
|
+
export type BuildEntryParams = {
|
|
68
|
+
runtime: Runtime;
|
|
69
|
+
key: CredentialKeyMaterial;
|
|
70
|
+
plaintext: Uint8Array;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Mint a key, its content key and one wrapping per recipient set.
|
|
74
|
+
*
|
|
75
|
+
* `ownerDID` is normalized because it is an equality key — a lookup and an
|
|
76
|
+
* authorization comparison both match on it — while a wrapping's recipient is
|
|
77
|
+
* not, because it is a key-resolution input and folding a `did:peer:4` long form
|
|
78
|
+
* would make the wrapping unopenable.
|
|
79
|
+
*/
|
|
80
|
+
export declare function buildCredentialKey(params: BuildKeyParams): Promise<BuiltKey>;
|
|
81
|
+
/** Encrypt one value under a key already built or already open. */
|
|
82
|
+
export declare function buildCredentialEntry(params: BuildEntryParams): BuiltEntry;
|
package/lib/builder.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { CredentialEntryID, CredentialKeyID } from '@kubun/id';
|
|
3
|
+
import { InvalidCredentialFactors } from './errors.js';
|
|
4
|
+
import { getSuite, SUITE_KUBUN_CRED_1 } from './suite.js';
|
|
5
|
+
/**
|
|
6
|
+
* Mint a key, its content key and one wrapping per recipient set.
|
|
7
|
+
*
|
|
8
|
+
* `ownerDID` is normalized because it is an equality key — a lookup and an
|
|
9
|
+
* authorization comparison both match on it — while a wrapping's recipient is
|
|
10
|
+
* not, because it is a key-resolution input and folding a `did:peer:4` long form
|
|
11
|
+
* would make the wrapping unopenable.
|
|
12
|
+
*/ export async function buildCredentialKey(params) {
|
|
13
|
+
if (params.wrappings.length === 0) {
|
|
14
|
+
throw new InvalidCredentialFactors('a key needs at least one wrapping');
|
|
15
|
+
}
|
|
16
|
+
const { runtime } = params;
|
|
17
|
+
const suiteID = params.suite ?? SUITE_KUBUN_CRED_1;
|
|
18
|
+
const suite = getSuite(suiteID);
|
|
19
|
+
const ownerDID = normalizeDID(params.ownerDID);
|
|
20
|
+
const version = 1;
|
|
21
|
+
const keyID = CredentialKeyID.create(ownerDID, runtime.getRandomValues(new Uint8Array(16))).toString();
|
|
22
|
+
const contentKey = suite.generateContentKey(runtime);
|
|
23
|
+
const wrappings = [];
|
|
24
|
+
for (const factors of params.wrappings){
|
|
25
|
+
const wrappingID = runtime.getRandomID();
|
|
26
|
+
const { descriptors, iv, wrappedKey } = await suite.wrap({
|
|
27
|
+
runtime,
|
|
28
|
+
contentKey,
|
|
29
|
+
factors,
|
|
30
|
+
wrappingID,
|
|
31
|
+
keyID,
|
|
32
|
+
keyVersion: version,
|
|
33
|
+
methods: params.methods
|
|
34
|
+
});
|
|
35
|
+
wrappings.push({
|
|
36
|
+
wrappingID,
|
|
37
|
+
keyID,
|
|
38
|
+
keyVersion: version,
|
|
39
|
+
factors: descriptors,
|
|
40
|
+
iv,
|
|
41
|
+
wrappedKey,
|
|
42
|
+
// Populated only for the single-`did` case. A `[did, pin]` wrapping is not
|
|
43
|
+
// openable by its recipient alone, so recording one would let a lookup
|
|
44
|
+
// conclude the key opens on that device when it does not.
|
|
45
|
+
recipientDID: factors.length === 1 && factors[0]?.kind === 'did' ? factors[0].recipientDID : null
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
keyID,
|
|
50
|
+
ownerDID,
|
|
51
|
+
suite: suiteID,
|
|
52
|
+
version,
|
|
53
|
+
contentKey,
|
|
54
|
+
wrappings
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Encrypt one value under a key already built or already open. */ export function buildCredentialEntry(params) {
|
|
58
|
+
const { runtime, key } = params;
|
|
59
|
+
const suite = getSuite(key.suite);
|
|
60
|
+
const entryID = CredentialEntryID.create(key.ownerDID, runtime.getRandomValues(new Uint8Array(16))).toString();
|
|
61
|
+
const { iv, ciphertext } = suite.encryptEntry({
|
|
62
|
+
runtime,
|
|
63
|
+
contentKey: key.contentKey,
|
|
64
|
+
plaintext: params.plaintext,
|
|
65
|
+
keyID: key.keyID,
|
|
66
|
+
keyVersion: key.version,
|
|
67
|
+
entryID
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
entryID,
|
|
71
|
+
keyID: key.keyID,
|
|
72
|
+
keyVersion: key.version,
|
|
73
|
+
iv,
|
|
74
|
+
ciphertext
|
|
75
|
+
};
|
|
76
|
+
}
|
package/lib/digest.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CredentialFactorDescriptor } from '@kubun/store-credential';
|
|
2
|
+
/**
|
|
3
|
+
* Length-prefixed, not delimited. A separator would only be unambiguous while
|
|
4
|
+
* every part stayed inside an alphabet that excludes it — which is true of
|
|
5
|
+
* base64url and not of a DID or a factor kind, so the framing would be a
|
|
6
|
+
* property of the inputs rather than of the digest.
|
|
7
|
+
*
|
|
8
|
+
* blake3 for the React Native / browser / node triple, as elsewhere here.
|
|
9
|
+
*/
|
|
10
|
+
export declare function digestParts(parts: Array<string>): string;
|
|
11
|
+
/** A key's public record, as the wire spells it. */
|
|
12
|
+
export type CredentialKeyDigestInput = {
|
|
13
|
+
ownerDID: string;
|
|
14
|
+
suite: number;
|
|
15
|
+
version: number;
|
|
16
|
+
state: string;
|
|
17
|
+
};
|
|
18
|
+
/** One wrapping, with its byte fields base64url-encoded. */
|
|
19
|
+
export type CredentialWrappingDigestInput = {
|
|
20
|
+
wrappingID: string;
|
|
21
|
+
/** `null` for a wrapping addressed to no DID — a passphrase-only one. */
|
|
22
|
+
recipientDID: string | null;
|
|
23
|
+
iv: string;
|
|
24
|
+
wrappedKey: string;
|
|
25
|
+
factors: Array<CredentialFactorDescriptor>;
|
|
26
|
+
};
|
|
27
|
+
/** One entry, with its byte fields base64url-encoded. */
|
|
28
|
+
export type CredentialEntryDigestInput = {
|
|
29
|
+
entryID: string;
|
|
30
|
+
iv: string;
|
|
31
|
+
ciphertext: string;
|
|
32
|
+
hlc: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Every field of the key's public record. `created_at` is absent deliberately:
|
|
36
|
+
* it is the local write time, and two devices holding the same key disagree
|
|
37
|
+
* about it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function credentialKeyDigest(key: CredentialKeyDigestInput): string;
|
|
40
|
+
/** Every field of the wrapping a receiver writes, including the factor descriptors. */
|
|
41
|
+
export declare function credentialWrappingDigest(wrapping: CredentialWrappingDigestInput): string;
|
|
42
|
+
/**
|
|
43
|
+
* Entries in `entryID` order, so the same set digests identically wherever it is
|
|
44
|
+
* assembled. `listEntries` order is the store's, and two devices need not agree
|
|
45
|
+
* on it.
|
|
46
|
+
*
|
|
47
|
+
* `hlc` contributes along with the ciphertext: the receiver writes it into the
|
|
48
|
+
* entry row, and an input the receiver stores that the signature does not cover
|
|
49
|
+
* is an unauthenticated write.
|
|
50
|
+
*/
|
|
51
|
+
export declare function credentialEntriesDigest(entries: ReadonlyArray<CredentialEntryDigestInput>): string;
|
|
52
|
+
/** One entry's digest is the one-element set's — the same framing, not a second one. */
|
|
53
|
+
export declare function credentialEntryDigest(entry: CredentialEntryDigestInput): string;
|
package/lib/digest.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { blake3 } from '@noble/hashes/blake3.js';
|
|
2
|
+
import { fromUTF, toB64U } from '@sozai/codec';
|
|
3
|
+
/**
|
|
4
|
+
* Canonical JSON: object keys sorted, array order kept.
|
|
5
|
+
*
|
|
6
|
+
* The two halves are deliberate. A `params` record is an unordered bag whose
|
|
7
|
+
* key order is whatever a JSON round trip happened to preserve, so digesting it
|
|
8
|
+
* as written would make the digest a property of the serializer. A `factors`
|
|
9
|
+
* array is not: the KEK derivation concatenates factor secrets in order and the
|
|
10
|
+
* descriptors are bound into the wrapping's AAD, so a permuted list is a
|
|
11
|
+
* different wrapping rather than the same one written differently.
|
|
12
|
+
*/ function canonicalJSON(value) {
|
|
13
|
+
if (value === null || typeof value !== 'object') {
|
|
14
|
+
return JSON.stringify(value) ?? 'null';
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return `[${value.map(canonicalJSON).join(',')}]`;
|
|
18
|
+
}
|
|
19
|
+
const entries = Object.entries(value).sort(([left], [right])=>left < right ? -1 : left > right ? 1 : 0);
|
|
20
|
+
return `{${entries.map(([key, item])=>`${JSON.stringify(key)}:${canonicalJSON(item)}`).join(',')}}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Length-prefixed, not delimited. A separator would only be unambiguous while
|
|
24
|
+
* every part stayed inside an alphabet that excludes it — which is true of
|
|
25
|
+
* base64url and not of a DID or a factor kind, so the framing would be a
|
|
26
|
+
* property of the inputs rather than of the digest.
|
|
27
|
+
*
|
|
28
|
+
* blake3 for the React Native / browser / node triple, as elsewhere here.
|
|
29
|
+
*/ export function digestParts(parts) {
|
|
30
|
+
const framed = parts.map((part)=>`${part.length}:${part}`).join('');
|
|
31
|
+
return toB64U(blake3(fromUTF(framed), {
|
|
32
|
+
dkLen: 32
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every field of the key's public record. `created_at` is absent deliberately:
|
|
37
|
+
* it is the local write time, and two devices holding the same key disagree
|
|
38
|
+
* about it.
|
|
39
|
+
*/ export function credentialKeyDigest(key) {
|
|
40
|
+
return digestParts([
|
|
41
|
+
key.ownerDID,
|
|
42
|
+
String(key.suite),
|
|
43
|
+
String(key.version),
|
|
44
|
+
key.state
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
/** Every field of the wrapping a receiver writes, including the factor descriptors. */ export function credentialWrappingDigest(wrapping) {
|
|
48
|
+
return digestParts([
|
|
49
|
+
wrapping.wrappingID,
|
|
50
|
+
// An absent recipient digests as an empty part rather than being skipped:
|
|
51
|
+
// the parts are length-prefixed, and no DID is the empty string, so the two
|
|
52
|
+
// cases stay distinguishable without a marker of their own.
|
|
53
|
+
wrapping.recipientDID ?? '',
|
|
54
|
+
wrapping.iv,
|
|
55
|
+
wrapping.wrappedKey,
|
|
56
|
+
canonicalJSON(wrapping.factors)
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Entries in `entryID` order, so the same set digests identically wherever it is
|
|
61
|
+
* assembled. `listEntries` order is the store's, and two devices need not agree
|
|
62
|
+
* on it.
|
|
63
|
+
*
|
|
64
|
+
* `hlc` contributes along with the ciphertext: the receiver writes it into the
|
|
65
|
+
* entry row, and an input the receiver stores that the signature does not cover
|
|
66
|
+
* is an unauthenticated write.
|
|
67
|
+
*/ export function credentialEntriesDigest(entries) {
|
|
68
|
+
const ordered = [
|
|
69
|
+
...entries
|
|
70
|
+
].sort((left, right)=>left.entryID < right.entryID ? -1 : left.entryID > right.entryID ? 1 : 0);
|
|
71
|
+
return digestParts(ordered.flatMap((entry)=>[
|
|
72
|
+
entry.entryID,
|
|
73
|
+
entry.iv,
|
|
74
|
+
entry.ciphertext,
|
|
75
|
+
entry.hlc
|
|
76
|
+
]));
|
|
77
|
+
}
|
|
78
|
+
/** One entry's digest is the one-element set's — the same framing, not a second one. */ export function credentialEntryDigest(entry) {
|
|
79
|
+
return credentialEntriesDigest([
|
|
80
|
+
entry
|
|
81
|
+
]);
|
|
82
|
+
}
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { CredentialAuthorization } from './authority.js';
|
|
2
|
+
/**
|
|
3
|
+
* A machine-readable name for a failure, stable across message rewording.
|
|
4
|
+
*
|
|
5
|
+
* A union rather than a loose string so that a new error class cannot be added
|
|
6
|
+
* without deciding what a caller is meant to match on. Messages are for people
|
|
7
|
+
* and change; these do not.
|
|
8
|
+
*/
|
|
9
|
+
export type CredentialErrorCode = 'CREDENTIAL_ERROR' | 'CREDENTIAL_FACTOR_UNAVAILABLE' | 'CREDENTIAL_FACTORS_INVALID' | 'CREDENTIAL_KEY_LOCKED' | 'CREDENTIAL_KEY_VERSION_MISMATCH' | 'CREDENTIAL_NOT_AUTHORIZED' | 'CREDENTIAL_NOT_FOUND' | 'CREDENTIAL_PIN_REJECTED' | 'CREDENTIAL_SECRET_REJECTED' | 'CREDENTIAL_SUPERSEDED_BRANCH' | 'CREDENTIAL_SUITE_UNSUPPORTED' | 'CREDENTIAL_TAMPERED' | 'CREDENTIAL_UNOPENABLE' | 'CREDENTIAL_WRAPPING_AMBIGUOUS';
|
|
10
|
+
export type CredentialErrorOptions = ErrorOptions & {
|
|
11
|
+
code: CredentialErrorCode;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Failures a caller acts on differently.
|
|
15
|
+
*
|
|
16
|
+
* The distinctions are not cosmetic. "This device cannot supply the factor" is a
|
|
17
|
+
* prompt-the-user or use-another-device outcome; "the descriptors were tampered
|
|
18
|
+
* with" is a security event; "wrong secret" is a retry. Collapsing them into one
|
|
19
|
+
* decrypt error is what makes a tampered wrapping indistinguishable from a
|
|
20
|
+
* mistyped PIN.
|
|
21
|
+
*
|
|
22
|
+
* `code` is required rather than defaulted: a subclass that inherited a generic
|
|
23
|
+
* code would compile, ship, and reach a client as an untyped failure, which is
|
|
24
|
+
* the one outcome this hierarchy exists to prevent.
|
|
25
|
+
*/
|
|
26
|
+
export declare class CredentialError extends Error {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(message: string, options: CredentialErrorOptions);
|
|
29
|
+
get code(): CredentialErrorCode;
|
|
30
|
+
}
|
|
31
|
+
/** A wrapping needs a factor kind this device cannot supply. */
|
|
32
|
+
export declare class NoAvailableFactor extends CredentialError {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(kind: string);
|
|
35
|
+
get kind(): string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A factor list a wrapping may not be built from.
|
|
39
|
+
*
|
|
40
|
+
* Raised at creation rather than at first unwrap: an unopenable or ambiguous
|
|
41
|
+
* wrapping that only fails on read is a key nobody notices is broken until they
|
|
42
|
+
* need it.
|
|
43
|
+
*/
|
|
44
|
+
export declare class InvalidCredentialFactors extends CredentialError {
|
|
45
|
+
constructor(reason: string);
|
|
46
|
+
}
|
|
47
|
+
/** A key names a ciphersuite this build does not implement. */
|
|
48
|
+
export declare class UnsupportedCredentialSuite extends CredentialError {
|
|
49
|
+
#private;
|
|
50
|
+
constructor(suite: number);
|
|
51
|
+
get suite(): number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A wrapping did not authenticate, and no user-supplied secret was involved.
|
|
55
|
+
*
|
|
56
|
+
* Raised for a wrapping whose factors are all `did`, where nothing the user
|
|
57
|
+
* typed could have been wrong. The causes left are an edited descriptor list,
|
|
58
|
+
* corrupted ciphertext, a wrapping copied onto another row — **and a wrapping
|
|
59
|
+
* addressed to someone else**. X25519 agreement never fails: a foreign recipient
|
|
60
|
+
* recovers different bytes rather than erroring, so the tag fails identically. A
|
|
61
|
+
* wrapping this device is not addressed to normally never reaches an unwrap at
|
|
62
|
+
* all, because `satisfiableFactors` filters it out first.
|
|
63
|
+
*/
|
|
64
|
+
export declare class CredentialTampered extends CredentialError {
|
|
65
|
+
constructor(message: string, options?: ErrorOptions);
|
|
66
|
+
}
|
|
67
|
+
/** An entry belongs to a branch superseded by the key's resolved winner. */
|
|
68
|
+
export declare class CredentialSupersededBranch extends CredentialError {
|
|
69
|
+
#private;
|
|
70
|
+
constructor(entryID: string, entryBranchID: string, winnerBranchID: string);
|
|
71
|
+
get entryID(): string;
|
|
72
|
+
get entryBranchID(): string;
|
|
73
|
+
get winnerBranchID(): string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A wrapping carrying a user-supplied factor did not authenticate.
|
|
77
|
+
*
|
|
78
|
+
* Attribution to a wrong secret is policy, not a cryptographic fact: one tag
|
|
79
|
+
* failure is all that is observable, and a tampered descriptor produces the same
|
|
80
|
+
* one. This is the retry path; the `pin` throttle bounds repeated attempts.
|
|
81
|
+
*/
|
|
82
|
+
export declare class CredentialSecretRejected extends CredentialError {
|
|
83
|
+
#private;
|
|
84
|
+
constructor(kinds: Array<string>, options?: ErrorOptions & {
|
|
85
|
+
keyID?: string;
|
|
86
|
+
code?: CredentialErrorCode;
|
|
87
|
+
});
|
|
88
|
+
/** The user-supplied factor kinds in the wrapping, any of which could be wrong. */
|
|
89
|
+
get kinds(): Array<string>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A PIN attempt was wrong, or refused because too many already were.
|
|
93
|
+
*
|
|
94
|
+
* Raised by the manager rather than the suite: the counter is per-key and the
|
|
95
|
+
* suite holds none. A refusal is issued before the KDF runs, so a throttled
|
|
96
|
+
* attempt costs the attacker time and the defender nothing.
|
|
97
|
+
*/
|
|
98
|
+
export declare class CredentialPinRejected extends CredentialSecretRejected {
|
|
99
|
+
#private;
|
|
100
|
+
constructor(keyID: string, options: ErrorOptions & {
|
|
101
|
+
attemptsRemaining: number;
|
|
102
|
+
retryAfter: number;
|
|
103
|
+
});
|
|
104
|
+
/** Attempts left before a delay begins. */
|
|
105
|
+
get attemptsRemaining(): number;
|
|
106
|
+
/** Milliseconds until another attempt is accepted. Zero means now. */
|
|
107
|
+
get retryAfter(): number;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* This identity holds no authority to do that to that key.
|
|
111
|
+
*
|
|
112
|
+
* Named rather than collapsed into {@link CredentialNotFound}, which is what a
|
|
113
|
+
* verb with no ownership check reports once it goes looking for a wrapping it
|
|
114
|
+
* was never entitled to touch. "There is no such key here" and "that key is not
|
|
115
|
+
* yours to administer" want opposite things from a caller, and only one of them
|
|
116
|
+
* is worth retrying on another device.
|
|
117
|
+
*/
|
|
118
|
+
export declare class CredentialNotAuthorized extends CredentialError {
|
|
119
|
+
#private;
|
|
120
|
+
constructor(authorization: CredentialAuthorization, keyID: string);
|
|
121
|
+
get authorization(): CredentialAuthorization;
|
|
122
|
+
}
|
|
123
|
+
/** No key, wrapping or entry under that id. */
|
|
124
|
+
export declare class CredentialNotFound extends CredentialError {
|
|
125
|
+
constructor(what: string, id: string);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* More than one wrapping on a key is addressed to this device.
|
|
129
|
+
*
|
|
130
|
+
* Thrown rather than resolved by picking: re-wrapping one of them would leave
|
|
131
|
+
* the other un-gated and look like success.
|
|
132
|
+
*/
|
|
133
|
+
export declare class AmbiguousCredentialWrapping extends CredentialError {
|
|
134
|
+
constructor(keyID: string, count: number);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The key is closed, and a factor this device can supply would open it.
|
|
138
|
+
*
|
|
139
|
+
* Distinct from {@link CredentialUnopenable} because the two need opposite
|
|
140
|
+
* responses: this one is a prompt, that one is "use another device". Collapsing
|
|
141
|
+
* them makes the UI ask for a PIN that will never work.
|
|
142
|
+
*/
|
|
143
|
+
export declare class CredentialKeyLocked extends CredentialError {
|
|
144
|
+
#private;
|
|
145
|
+
constructor(keyID: string, kinds: Array<string>);
|
|
146
|
+
get keyID(): string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* An entry was written under a key version the key has moved past.
|
|
150
|
+
*
|
|
151
|
+
* Rotation re-encrypts every entry, so this is a rotation that crashed partway.
|
|
152
|
+
* Loud by design: falling back to the older version would read a value the
|
|
153
|
+
* rotation was meant to retire, and silently.
|
|
154
|
+
*/
|
|
155
|
+
export declare class CredentialKeyVersionMismatch extends CredentialError {
|
|
156
|
+
constructor(entryID: string, entryVersion: number, keyVersion: number);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Every wrapping for the key was tried and none opened.
|
|
160
|
+
*
|
|
161
|
+
* Reported rather than skipped: a key whose only wrapping uses an unimplemented
|
|
162
|
+
* suite, or whose factors this device lacks, is a state the caller must be able
|
|
163
|
+
* to show. Silently returning "no entries" would read as an empty vault.
|
|
164
|
+
*/
|
|
165
|
+
export declare class CredentialUnopenable extends CredentialError {
|
|
166
|
+
#private;
|
|
167
|
+
constructor(keyID: string, reason: string);
|
|
168
|
+
get keyID(): string;
|
|
169
|
+
}
|