@byok-sdk/core 0.29.0 → 0.30.1
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/dist/index.js +18 -9
- package/dist/index.js.map +1 -1
- package/dist/sealed-payload/index.d.ts +10 -0
- package/dist/sealed-payload/index.js +573 -26
- package/dist/sealed-payload/index.js.map +1 -1
- package/dist/sealed-payload/member-bundle.d.ts +64 -0
- package/dist/sealed-payload/member-chain-walk.d.ts +77 -0
- package/dist/sealed-payload/member-fingerprint.d.ts +14 -0
- package/dist/sealed-payload/member-key-record.d.ts +61 -0
- package/dist/sealed-payload/pairing-verification.d.ts +51 -0
- package/dist/sealed-payload/sealed-blob.d.ts +94 -0
- package/dist/sealed-payload/sealed-content.d.ts +3 -2
- package/dist/sealed-payload/sealed-json.d.ts +17 -0
- package/dist/total-parse.d.ts +20 -0
- package/package.json +1 -1
|
@@ -137,6 +137,15 @@ function isCanonicalTimestamp(value) {
|
|
|
137
137
|
if (!Number.isFinite(parsed)) return false;
|
|
138
138
|
return new Date(parsed).toISOString() === value;
|
|
139
139
|
}
|
|
140
|
+
|
|
141
|
+
// src/total-parse.ts
|
|
142
|
+
function safeParseTotal(schema, value) {
|
|
143
|
+
try {
|
|
144
|
+
return schema.safeParse(value);
|
|
145
|
+
} catch {
|
|
146
|
+
return { success: false };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
140
149
|
function webCryptoSubtle() {
|
|
141
150
|
const subtle = globalThis.crypto?.subtle;
|
|
142
151
|
if (subtle === void 0) {
|
|
@@ -388,7 +397,7 @@ var SealingPublicJwkSchema = z.strictObject({
|
|
|
388
397
|
});
|
|
389
398
|
var SealingKeyIdSchema = z.string().regex(new RegExp(`^[A-Za-z0-9_-]{${SEALING_KEY_ID_LENGTH}}$`));
|
|
390
399
|
function parseSealingPublicJwk(input) {
|
|
391
|
-
const result = SealingPublicJwkSchema
|
|
400
|
+
const result = safeParseTotal(SealingPublicJwkSchema, input);
|
|
392
401
|
if (!result.success) {
|
|
393
402
|
throw new ByokCoreError("sealed_secret_invalid", "Sealing public key must be a strict P-256 EC public JWK");
|
|
394
403
|
}
|
|
@@ -597,15 +606,16 @@ var MEMBER_ID_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
|
597
606
|
var MEMBER_CHAIN_VERSION = 1;
|
|
598
607
|
var MEMBER_CHAIN_DOMAIN_PREFIX = "byok-member-chain-v1\n";
|
|
599
608
|
var MEMBER_CHAIN_OPS = ["add", "remove", "rotate", "scope-policy", "shred"];
|
|
600
|
-
var CanonicalJsonObjectSchema = z.
|
|
601
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
609
|
+
var CanonicalJsonObjectSchema = z.unknown().transform((value, ctx) => {
|
|
602
610
|
try {
|
|
603
|
-
|
|
604
|
-
|
|
611
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
612
|
+
return JSON.parse(canonicalizeJson(value));
|
|
613
|
+
}
|
|
605
614
|
} catch {
|
|
606
|
-
return false;
|
|
607
615
|
}
|
|
608
|
-
|
|
616
|
+
ctx.addIssue({ code: "custom", message: "must be a canonical JSON object" });
|
|
617
|
+
return z.NEVER;
|
|
618
|
+
});
|
|
609
619
|
var unsignedEntryShape = {
|
|
610
620
|
v: z.literal(MEMBER_CHAIN_VERSION),
|
|
611
621
|
tenantId: z.string().min(1),
|
|
@@ -627,7 +637,7 @@ async function deriveMemberId(signingPublicJwk) {
|
|
|
627
637
|
return encodeBase64Url(await sha256(concat2(utf82(MEMBER_ID_DOMAIN), raw)));
|
|
628
638
|
}
|
|
629
639
|
function parseUnsignedEntry(entry) {
|
|
630
|
-
const parsed = UnsignedMemberChainEntryV1Schema
|
|
640
|
+
const parsed = safeParseTotal(UnsignedMemberChainEntryV1Schema, entry);
|
|
631
641
|
if (!parsed.success) throw invalidInput("Member chain entry is invalid");
|
|
632
642
|
return parsed.data;
|
|
633
643
|
}
|
|
@@ -659,7 +669,7 @@ async function signMemberChainEntry(entry, signer) {
|
|
|
659
669
|
return { ...unsigned, sig: await ecdsaSign(signer.publicJwk, signer.privateKey, signingInput(unsigned)) };
|
|
660
670
|
}
|
|
661
671
|
async function verifyMemberChainEntry(entry, signerPublicJwk) {
|
|
662
|
-
const parsed = MemberChainEntryV1Schema
|
|
672
|
+
const parsed = safeParseTotal(MemberChainEntryV1Schema, entry);
|
|
663
673
|
if (!parsed.success || await deriveMemberId(signerPublicJwk) !== parsed.data.signer) {
|
|
664
674
|
return { ok: false, reason: "member_chain_entry_invalid" };
|
|
665
675
|
}
|
|
@@ -741,7 +751,7 @@ function packageSigningInput(pkg) {
|
|
|
741
751
|
}
|
|
742
752
|
var PackageIdentitySchema = ScopeKeyPackageV1Schema.omit({ enc: true, ciphertext: true, sig: true });
|
|
743
753
|
async function sealScopeKeyPackage(input) {
|
|
744
|
-
const parsed = PackageIdentitySchema
|
|
754
|
+
const parsed = safeParseTotal(PackageIdentitySchema, {
|
|
745
755
|
v: SCOPE_KEY_PACKAGE_VERSION,
|
|
746
756
|
tenantId: input.tenantId,
|
|
747
757
|
scopeId: input.scopeId,
|
|
@@ -762,7 +772,7 @@ async function sealScopeKeyPackage(input) {
|
|
|
762
772
|
return { ...unsigned, sig: await ecdsaSign(input.sender.signingPublicJwk, input.sender.signingPrivateKey, packageSigningInput(unsigned)) };
|
|
763
773
|
}
|
|
764
774
|
async function openScopeKeyPackage(input) {
|
|
765
|
-
const parsed = ScopeKeyPackageV1Schema
|
|
775
|
+
const parsed = safeParseTotal(ScopeKeyPackageV1Schema, input.package);
|
|
766
776
|
if (!parsed.success) return { ok: false, reason: "key_package_invalid" };
|
|
767
777
|
const pkg = parsed.data;
|
|
768
778
|
if (pkg.recipient !== input.recipient.memberId || pkg.tenantId !== input.expected.tenantId || pkg.scopeId !== input.expected.scopeId || pkg.epoch !== input.expected.epoch || pkg.sender !== await deriveMemberId(input.senderSigningPublicJwk)) {
|
|
@@ -792,6 +802,39 @@ async function openScopeKeyPackage(input) {
|
|
|
792
802
|
return { ok: true, scopeKey, keyId: pkg.keyId };
|
|
793
803
|
}
|
|
794
804
|
|
|
805
|
+
// src/sealed-payload/sealed-json.ts
|
|
806
|
+
function canonicalValue(value, seen) {
|
|
807
|
+
if (value === null) return "null";
|
|
808
|
+
switch (typeof value) {
|
|
809
|
+
case "boolean":
|
|
810
|
+
return value ? "true" : "false";
|
|
811
|
+
case "string":
|
|
812
|
+
return JSON.stringify(value);
|
|
813
|
+
case "number":
|
|
814
|
+
if (!Number.isFinite(value)) throw invalidInput("Sealed plaintext holds a number with no JSON form");
|
|
815
|
+
return String(value);
|
|
816
|
+
case "object":
|
|
817
|
+
break;
|
|
818
|
+
default:
|
|
819
|
+
throw invalidInput(`Sealed plaintext holds a ${typeof value}, which is not a JSON type`);
|
|
820
|
+
}
|
|
821
|
+
if (seen.has(value)) throw invalidInput("Sealed plaintext holds a circular reference");
|
|
822
|
+
seen.add(value);
|
|
823
|
+
try {
|
|
824
|
+
if (Array.isArray(value)) return `[${Array.from(value, (entry) => canonicalValue(entry, seen)).join(",")}]`;
|
|
825
|
+
const prototype = Object.getPrototypeOf(value);
|
|
826
|
+
if (prototype !== Object.prototype && prototype !== null) throw invalidInput("Sealed plaintext holds an object that is not plain");
|
|
827
|
+
const record = value;
|
|
828
|
+
const members = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalValue(record[key], seen)}`);
|
|
829
|
+
return `{${members.join(",")}}`;
|
|
830
|
+
} finally {
|
|
831
|
+
seen.delete(value);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function canonicalizeSealedJson(value) {
|
|
835
|
+
return canonicalValue(value, /* @__PURE__ */ new Set());
|
|
836
|
+
}
|
|
837
|
+
|
|
795
838
|
// src/sealed-payload/sealed-content.ts
|
|
796
839
|
var SEALED_CONTENT_VERSION = 1;
|
|
797
840
|
var SEALED_CONTENT_SUITE = "byok-sealed-v1.hkdf-sha256.aes-256-gcm";
|
|
@@ -890,7 +933,7 @@ var SealedBindingSchema = z.strictObject({
|
|
|
890
933
|
}
|
|
891
934
|
});
|
|
892
935
|
function parseBinding(binding) {
|
|
893
|
-
const parsed = SealedBindingSchema
|
|
936
|
+
const parsed = safeParseTotal(SealedBindingSchema, binding);
|
|
894
937
|
if (!parsed.success) throw invalidInput("Sealed content binding is invalid");
|
|
895
938
|
return parsed.data;
|
|
896
939
|
}
|
|
@@ -899,8 +942,8 @@ function assertMaxContentLength(maxContentLength) {
|
|
|
899
942
|
throw invalidInput("Sealed content limit must be a non-negative 32-bit integer");
|
|
900
943
|
}
|
|
901
944
|
}
|
|
902
|
-
function
|
|
903
|
-
return
|
|
945
|
+
function bindingAad(header, binding) {
|
|
946
|
+
return {
|
|
904
947
|
v: header.v,
|
|
905
948
|
suite: header.suite,
|
|
906
949
|
category: header.category,
|
|
@@ -916,10 +959,11 @@ function aadBytes(header, binding) {
|
|
|
916
959
|
deviceId: binding.deviceId,
|
|
917
960
|
agentRef: binding.agentRef === null ? null : { agentId: binding.agentRef.agentId, profileRevision: binding.agentRef.profileRevision },
|
|
918
961
|
categories: [...binding.categories],
|
|
919
|
-
notAfter: binding.notAfter
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
962
|
+
notAfter: binding.notAfter
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function aadBytes(header, binding) {
|
|
966
|
+
return canonicalizeJsonBytes({ ...bindingAad(header, binding), paddedLength: header.paddedLength, contentMac: header.contentMac });
|
|
923
967
|
}
|
|
924
968
|
function isNonEmptyPlainObject(value) {
|
|
925
969
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
@@ -927,12 +971,12 @@ function isNonEmptyPlainObject(value) {
|
|
|
927
971
|
return (prototype === Object.prototype || prototype === null) && Object.keys(value).length > 0;
|
|
928
972
|
}
|
|
929
973
|
function canonicalPlaintext(plaintext) {
|
|
930
|
-
if (!isNonEmptyPlainObject(plaintext)) throw invalidInput("Sealed plaintext must be a non-empty plain JSON object");
|
|
931
974
|
try {
|
|
932
|
-
return
|
|
975
|
+
if (isNonEmptyPlainObject(plaintext)) return canonicalizeSealedJson(plaintext);
|
|
933
976
|
} catch (cause) {
|
|
934
977
|
throw new ByokCoreError("sealed_secret_invalid", "Sealed plaintext is not canonical JSON", { cause });
|
|
935
978
|
}
|
|
979
|
+
throw invalidInput("Sealed plaintext must be a non-empty plain JSON object");
|
|
936
980
|
}
|
|
937
981
|
function parseCanonicalPlaintext(content) {
|
|
938
982
|
let text;
|
|
@@ -940,7 +984,7 @@ function parseCanonicalPlaintext(content) {
|
|
|
940
984
|
try {
|
|
941
985
|
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(content);
|
|
942
986
|
value = JSON.parse(text);
|
|
943
|
-
if (!isNonEmptyPlainObject(value) ||
|
|
987
|
+
if (!isNonEmptyPlainObject(value) || canonicalizeSealedJson(value) !== text) return void 0;
|
|
944
988
|
} catch {
|
|
945
989
|
return void 0;
|
|
946
990
|
}
|
|
@@ -979,7 +1023,7 @@ async function sealContentWithRandomness(input, randomness) {
|
|
|
979
1023
|
if (content.byteLength > input.maxContentLength) throw invalidInput("Sealed content exceeds its plaintext limit");
|
|
980
1024
|
const frame = encodeSealedFrame(content);
|
|
981
1025
|
const { salt, nonce } = randomness;
|
|
982
|
-
const parsed = SealedContentHeaderV1Schema
|
|
1026
|
+
const parsed = safeParseTotal(SealedContentHeaderV1Schema, {
|
|
983
1027
|
v: SEALED_CONTENT_VERSION,
|
|
984
1028
|
suite: SEALED_CONTENT_SUITE,
|
|
985
1029
|
category: input.category,
|
|
@@ -1008,12 +1052,12 @@ function failure(reason) {
|
|
|
1008
1052
|
}
|
|
1009
1053
|
async function openSealedContent(input) {
|
|
1010
1054
|
assertMaxContentLength(input.maxContentLength);
|
|
1011
|
-
const parsedBinding = SealedBindingSchema
|
|
1012
|
-
const parsedSealing = SealedScopePolicySchema
|
|
1055
|
+
const parsedBinding = safeParseTotal(SealedBindingSchema, input.binding);
|
|
1056
|
+
const parsedSealing = safeParseTotal(SealedScopePolicySchema, input.sealing);
|
|
1013
1057
|
if (!parsedBinding.success || !parsedSealing.success) return failure("sealing_policy_mismatch");
|
|
1014
1058
|
const binding = parsedBinding.data;
|
|
1015
1059
|
const sealing = parsedSealing.data;
|
|
1016
|
-
const parsed = SealedContentHeaderV1Schema
|
|
1060
|
+
const parsed = safeParseTotal(SealedContentHeaderV1Schema, input.header);
|
|
1017
1061
|
if (!parsed.success) return failure("sealed_open_failed");
|
|
1018
1062
|
const header = parsed.data;
|
|
1019
1063
|
if (!binding.categories.includes(header.category) || header.scope.id !== sealing.scopeId || header.scope.epoch < sealing.epoch) {
|
|
@@ -1042,7 +1086,510 @@ async function openSealedContent(input) {
|
|
|
1042
1086
|
frame.fill(0);
|
|
1043
1087
|
}
|
|
1044
1088
|
}
|
|
1089
|
+
var MEMBER_KEY_BUNDLE_VERSION = 1;
|
|
1090
|
+
var MEMBER_KEM_DOMAIN_PREFIX = "byok-member-kem-v1\n";
|
|
1091
|
+
var KemEpochSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
1092
|
+
var KemSigSchema = base64UrlBytes(64);
|
|
1093
|
+
var MemberKeyBundleV1Schema = z.strictObject({
|
|
1094
|
+
v: z.literal(MEMBER_KEY_BUNDLE_VERSION),
|
|
1095
|
+
memberId: z.string().regex(MEMBER_ID_PATTERN),
|
|
1096
|
+
signingPublicJwk: P256PublicJwkSchema,
|
|
1097
|
+
kemPublicJwk: P256PublicJwkSchema,
|
|
1098
|
+
kemEpoch: KemEpochSchema,
|
|
1099
|
+
kemSig: KemSigSchema,
|
|
1100
|
+
deviceId: z.string().min(1).optional()
|
|
1101
|
+
});
|
|
1102
|
+
var CreateInputSchema = z.object({
|
|
1103
|
+
signingPublicJwk: P256PublicJwkSchema,
|
|
1104
|
+
signingPrivateKey: z.custom(),
|
|
1105
|
+
kemPublicJwk: P256PublicJwkSchema,
|
|
1106
|
+
kemEpoch: KemEpochSchema,
|
|
1107
|
+
deviceId: z.string().min(1).optional()
|
|
1108
|
+
});
|
|
1109
|
+
var KemSignerSchema = z.object({ memberId: z.string().regex(MEMBER_ID_PATTERN), signingPublicJwk: P256PublicJwkSchema });
|
|
1110
|
+
var KemStatementSchema = z.object({ kemPublicJwk: P256PublicJwkSchema, kemEpoch: KemEpochSchema, kemSig: KemSigSchema });
|
|
1111
|
+
function memberKemSigningInput(memberId, kemPublicJwk, kemEpoch) {
|
|
1112
|
+
return concat2(
|
|
1113
|
+
utf82(MEMBER_KEM_DOMAIN_PREFIX),
|
|
1114
|
+
canonicalizeJsonBytes({ memberId, kemPublicKey: { x: kemPublicJwk.x, y: kemPublicJwk.y }, kemEpoch })
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
async function createMemberKeyBundle(input) {
|
|
1118
|
+
const parsed = safeParseTotal(CreateInputSchema, input);
|
|
1119
|
+
if (!parsed.success) throw invalidInput("Member key bundle input is invalid");
|
|
1120
|
+
const { signingPublicJwk, signingPrivateKey, kemPublicJwk, kemEpoch, deviceId } = parsed.data;
|
|
1121
|
+
const memberId = await deriveMemberId(signingPublicJwk);
|
|
1122
|
+
if (!await isValidMemberKemKey(kemPublicJwk, signingPublicJwk)) throw invalidInput("Member KEM key is invalid");
|
|
1123
|
+
const kemSig = await ecdsaSign(signingPublicJwk, signingPrivateKey, memberKemSigningInput(memberId, kemPublicJwk, kemEpoch));
|
|
1124
|
+
return {
|
|
1125
|
+
v: MEMBER_KEY_BUNDLE_VERSION,
|
|
1126
|
+
memberId,
|
|
1127
|
+
signingPublicJwk,
|
|
1128
|
+
kemPublicJwk,
|
|
1129
|
+
kemEpoch,
|
|
1130
|
+
kemSig,
|
|
1131
|
+
...deviceId === void 0 ? {} : { deviceId }
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
async function isValidMemberKemKey(kemPublicJwk, signingPublicJwk) {
|
|
1135
|
+
if (kemPublicJwk.x === signingPublicJwk.x && kemPublicJwk.y === signingPublicJwk.y) return false;
|
|
1136
|
+
try {
|
|
1137
|
+
await p256PublicKeyBytes(kemPublicJwk);
|
|
1138
|
+
return true;
|
|
1139
|
+
} catch {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async function verifyMemberKem(member, kem) {
|
|
1144
|
+
const signer = safeParseTotal(KemSignerSchema, member);
|
|
1145
|
+
const statement = safeParseTotal(KemStatementSchema, kem);
|
|
1146
|
+
if (!signer.success || !statement.success) return false;
|
|
1147
|
+
const { memberId, signingPublicJwk } = signer.data;
|
|
1148
|
+
const { kemPublicJwk, kemEpoch, kemSig } = statement.data;
|
|
1149
|
+
if (!await isValidMemberKemKey(kemPublicJwk, signingPublicJwk)) return false;
|
|
1150
|
+
try {
|
|
1151
|
+
return await ecdsaVerify(signingPublicJwk, kemSig, memberKemSigningInput(memberId, kemPublicJwk, kemEpoch));
|
|
1152
|
+
} catch {
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async function parseMemberKeyBundle(value) {
|
|
1157
|
+
const parsed = safeParseTotal(MemberKeyBundleV1Schema, value);
|
|
1158
|
+
if (!parsed.success) return void 0;
|
|
1159
|
+
const bundle = parsed.data;
|
|
1160
|
+
try {
|
|
1161
|
+
if (await deriveMemberId(bundle.signingPublicJwk) !== bundle.memberId) return void 0;
|
|
1162
|
+
} catch {
|
|
1163
|
+
return void 0;
|
|
1164
|
+
}
|
|
1165
|
+
if (!await verifyMemberKem(bundle, bundle)) return void 0;
|
|
1166
|
+
return bundle;
|
|
1167
|
+
}
|
|
1168
|
+
var MEMBER_KEY_RECORD_VERSION = 1;
|
|
1169
|
+
var PrivateJwkSchema = z.strictObject({
|
|
1170
|
+
kty: z.literal("EC"),
|
|
1171
|
+
crv: z.literal("P-256"),
|
|
1172
|
+
x: base64UrlBytes(32),
|
|
1173
|
+
y: base64UrlBytes(32),
|
|
1174
|
+
d: base64UrlBytes(32)
|
|
1175
|
+
});
|
|
1176
|
+
var KemEpochSchema2 = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
1177
|
+
var MemberKeyRecordV1Schema = z.strictObject({
|
|
1178
|
+
v: z.literal(MEMBER_KEY_RECORD_VERSION),
|
|
1179
|
+
tenantId: z.string().min(1),
|
|
1180
|
+
deviceId: z.string().min(1).optional(),
|
|
1181
|
+
signingJwk: PrivateJwkSchema,
|
|
1182
|
+
kemEpoch: KemEpochSchema2,
|
|
1183
|
+
kemJwk: PrivateJwkSchema,
|
|
1184
|
+
nextKem: z.strictObject({ epoch: KemEpochSchema2, jwk: PrivateJwkSchema }).optional()
|
|
1185
|
+
}).refine((record) => record.nextKem === void 0 || record.nextKem.epoch === record.kemEpoch + 1);
|
|
1186
|
+
var BindingSchema = z.object({ tenantId: z.string().min(1), deviceId: z.string().min(1).optional() });
|
|
1187
|
+
function keySubtle() {
|
|
1188
|
+
return webCryptoSubtle();
|
|
1189
|
+
}
|
|
1190
|
+
var ECDSA_P2562 = { name: "ECDSA", namedCurve: "P-256" };
|
|
1191
|
+
var ECDSA_SHA2562 = { name: "ECDSA", hash: "SHA-256" };
|
|
1192
|
+
var ECDH_P256 = { name: "ECDH", namedCurve: "P-256" };
|
|
1193
|
+
async function generatePrivateJwk(algorithm, usages) {
|
|
1194
|
+
const pair = await keySubtle().generateKey(algorithm, true, usages);
|
|
1195
|
+
const { kty, crv, x, y, d } = await keySubtle().exportKey("jwk", pair.privateKey);
|
|
1196
|
+
return PrivateJwkSchema.parse({ kty, crv, x, y, d });
|
|
1197
|
+
}
|
|
1198
|
+
async function generateMemberKeyRecord(binding) {
|
|
1199
|
+
const parsed = safeParseTotal(BindingSchema, binding);
|
|
1200
|
+
if (!parsed.success) throw invalidInput("Member key binding is invalid");
|
|
1201
|
+
const { tenantId, deviceId } = parsed.data;
|
|
1202
|
+
return canonicalizeJson({
|
|
1203
|
+
v: MEMBER_KEY_RECORD_VERSION,
|
|
1204
|
+
tenantId,
|
|
1205
|
+
...deviceId === void 0 ? {} : { deviceId },
|
|
1206
|
+
signingJwk: await generatePrivateJwk(ECDSA_P2562, ["sign", "verify"]),
|
|
1207
|
+
kemEpoch: 1,
|
|
1208
|
+
kemJwk: await generatePrivateJwk(ECDH_P256, ["deriveBits"])
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
function publicJwkOf(jwk) {
|
|
1212
|
+
return P256PublicJwkSchema.parse({ kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y });
|
|
1213
|
+
}
|
|
1214
|
+
function bytesEqual(left, right) {
|
|
1215
|
+
const a = new Uint8Array(left);
|
|
1216
|
+
const b = new Uint8Array(right);
|
|
1217
|
+
return a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
1218
|
+
}
|
|
1219
|
+
async function importSigningKey(jwk) {
|
|
1220
|
+
const subtle = keySubtle();
|
|
1221
|
+
const publicJwk = publicJwkOf(jwk);
|
|
1222
|
+
const privateKey = await subtle.importKey("jwk", { ...jwk, ext: false }, ECDSA_P2562, false, ["sign"]);
|
|
1223
|
+
const publicKey = await subtle.importKey("jwk", publicJwk, ECDSA_P2562, true, ["verify"]);
|
|
1224
|
+
const probe = randomBytes(32);
|
|
1225
|
+
if (!await subtle.verify(ECDSA_SHA2562, publicKey, await subtle.sign(ECDSA_SHA2562, privateKey, probe), probe)) {
|
|
1226
|
+
throw new TypeError("signing scalar does not match its public point");
|
|
1227
|
+
}
|
|
1228
|
+
return { publicJwk, privateKey };
|
|
1229
|
+
}
|
|
1230
|
+
async function importKemKey(jwk) {
|
|
1231
|
+
const subtle = keySubtle();
|
|
1232
|
+
const publicJwk = publicJwkOf(jwk);
|
|
1233
|
+
const privateKey = await subtle.importKey("jwk", { ...jwk, ext: false }, ECDH_P256, false, ["deriveBits"]);
|
|
1234
|
+
const publicKey = await subtle.importKey("jwk", publicJwk, ECDH_P256, true, []);
|
|
1235
|
+
const probe = await subtle.generateKey(ECDH_P256, false, ["deriveBits"]);
|
|
1236
|
+
const viaMember = await subtle.deriveBits({ name: "ECDH", public: probe.publicKey }, privateKey, 256);
|
|
1237
|
+
const viaProbe = await subtle.deriveBits({ name: "ECDH", public: publicKey }, probe.privateKey, 256);
|
|
1238
|
+
if (!bytesEqual(viaMember, viaProbe)) throw new TypeError("KEM scalar does not match its public point");
|
|
1239
|
+
return { publicJwk, privateKey };
|
|
1240
|
+
}
|
|
1241
|
+
async function openMemberKeyRecord(text) {
|
|
1242
|
+
const record = parseRecord(text);
|
|
1243
|
+
if (record === void 0) return void 0;
|
|
1244
|
+
try {
|
|
1245
|
+
const signing = await importSigningKey(record.signingJwk);
|
|
1246
|
+
const kem = { epoch: record.kemEpoch, ...await importKemKey(record.kemJwk) };
|
|
1247
|
+
const binding = record.deviceId === void 0 ? { tenantId: record.tenantId } : { tenantId: record.tenantId, deviceId: record.deviceId };
|
|
1248
|
+
if (record.nextKem === void 0) return { binding, signing, kem };
|
|
1249
|
+
return { binding, signing, kem, nextKem: { epoch: record.nextKem.epoch, ...await importKemKey(record.nextKem.jwk) } };
|
|
1250
|
+
} catch {
|
|
1251
|
+
return void 0;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function beginMemberKemRotation(text) {
|
|
1255
|
+
const record = parseRecord(text);
|
|
1256
|
+
if (record === void 0) throw invalidInput("Member key record is invalid");
|
|
1257
|
+
if (record.nextKem !== void 0) return text;
|
|
1258
|
+
return canonicalizeJson({ ...record, nextKem: { epoch: record.kemEpoch + 1, jwk: await generatePrivateJwk(ECDH_P256, ["deriveBits"]) } });
|
|
1259
|
+
}
|
|
1260
|
+
function finishMemberKemRotation(text, epoch) {
|
|
1261
|
+
const record = parseRecord(text);
|
|
1262
|
+
if (record === void 0) throw invalidInput("Member key record is invalid");
|
|
1263
|
+
if (record.nextKem === void 0) return record.kemEpoch === epoch ? text : void 0;
|
|
1264
|
+
if (record.nextKem.epoch !== epoch) return void 0;
|
|
1265
|
+
const { nextKem, ...rest } = record;
|
|
1266
|
+
return canonicalizeJson({ ...rest, kemEpoch: nextKem.epoch, kemJwk: nextKem.jwk });
|
|
1267
|
+
}
|
|
1268
|
+
function parseRecord(text) {
|
|
1269
|
+
if (typeof text !== "string") return void 0;
|
|
1270
|
+
try {
|
|
1271
|
+
const parsed = safeParseTotal(MemberKeyRecordV1Schema, JSON.parse(text));
|
|
1272
|
+
return parsed.success && canonicalizeJson(parsed.data) === text ? parsed.data : void 0;
|
|
1273
|
+
} catch {
|
|
1274
|
+
return void 0;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
var MEMBER_FINGERPRINT_DOMAIN = "byok-member-fingerprint-v1\n";
|
|
1278
|
+
var FINGERPRINT_GROUPS = 5;
|
|
1279
|
+
var GROUP_BYTES = 5;
|
|
1280
|
+
var GROUP_MODULUS = 100000n;
|
|
1281
|
+
var FingerprintInputSchema = z.object({ tenantId: z.string().min(1), signingPublicJwk: P256PublicJwkSchema });
|
|
1282
|
+
async function memberFingerprint(input) {
|
|
1283
|
+
return (await fingerprintGroups(input)).groups.join(" ");
|
|
1284
|
+
}
|
|
1285
|
+
async function memberApprovalCode(tenantId, first, second) {
|
|
1286
|
+
const members = await Promise.all([first, second].map((signingPublicJwk) => fingerprintGroups({ tenantId, signingPublicJwk })));
|
|
1287
|
+
if (members[0].memberId === members[1].memberId) throw invalidInput("An approval code needs two different members");
|
|
1288
|
+
members.sort((a, b) => a.memberId < b.memberId ? -1 : 1);
|
|
1289
|
+
return members.flatMap((member) => member.groups).join(" ");
|
|
1290
|
+
}
|
|
1291
|
+
async function fingerprintGroups(input) {
|
|
1292
|
+
const parsed = safeParseTotal(FingerprintInputSchema, input);
|
|
1293
|
+
if (!parsed.success) throw invalidInput("Member fingerprint needs a tenant id and a P-256 signing key");
|
|
1294
|
+
const { tenantId, signingPublicJwk } = parsed.data;
|
|
1295
|
+
const memberId = await deriveMemberId(signingPublicJwk);
|
|
1296
|
+
const digest = await sha256(
|
|
1297
|
+
concat2(
|
|
1298
|
+
utf82(MEMBER_FINGERPRINT_DOMAIN),
|
|
1299
|
+
canonicalizeJsonBytes({ v: 1, tenantId, signingKey: { x: signingPublicJwk.x, y: signingPublicJwk.y } })
|
|
1300
|
+
)
|
|
1301
|
+
);
|
|
1302
|
+
const groups = [];
|
|
1303
|
+
for (let group = 0; group < FINGERPRINT_GROUPS; group += 1) {
|
|
1304
|
+
let value = 0n;
|
|
1305
|
+
for (const byte of digest.subarray(group * GROUP_BYTES, (group + 1) * GROUP_BYTES)) value = value << 8n | BigInt(byte);
|
|
1306
|
+
groups.push((value % GROUP_MODULUS).toString().padStart(5, "0"));
|
|
1307
|
+
}
|
|
1308
|
+
return { memberId, groups };
|
|
1309
|
+
}
|
|
1310
|
+
var PAIRING_VERIFICATION_SECRET_BYTES = 16;
|
|
1311
|
+
var PAIRING_VERIFICATION_TEXT_LENGTH = 26;
|
|
1312
|
+
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
1313
|
+
var PV_TEXT_PATTERN = /^[A-Za-z2-7]{26}$/u;
|
|
1314
|
+
function generatePairingVerificationSecret() {
|
|
1315
|
+
return randomBytes(PAIRING_VERIFICATION_SECRET_BYTES);
|
|
1316
|
+
}
|
|
1317
|
+
function encodePairingVerificationSecret(pv) {
|
|
1318
|
+
if (pv.byteLength !== PAIRING_VERIFICATION_SECRET_BYTES) {
|
|
1319
|
+
throw invalidInput(`Pairing verification secret must be ${PAIRING_VERIFICATION_SECRET_BYTES} bytes`);
|
|
1320
|
+
}
|
|
1321
|
+
let out = "";
|
|
1322
|
+
let buffer = 0;
|
|
1323
|
+
let bits = 0;
|
|
1324
|
+
for (const byte of pv) {
|
|
1325
|
+
buffer = buffer << 8 | byte;
|
|
1326
|
+
bits += 8;
|
|
1327
|
+
while (bits >= 5) {
|
|
1328
|
+
bits -= 5;
|
|
1329
|
+
out += BASE32_ALPHABET[buffer >>> bits & 31];
|
|
1330
|
+
}
|
|
1331
|
+
buffer &= (1 << bits) - 1;
|
|
1332
|
+
}
|
|
1333
|
+
return out + BASE32_ALPHABET[buffer << 5 - bits & 31];
|
|
1334
|
+
}
|
|
1335
|
+
function decodePairingVerificationSecret(text) {
|
|
1336
|
+
if (!PV_TEXT_PATTERN.test(text)) {
|
|
1337
|
+
throw invalidInput(`Pairing verification secret must be ${PAIRING_VERIFICATION_TEXT_LENGTH} base32 characters`);
|
|
1338
|
+
}
|
|
1339
|
+
const upper = text.toUpperCase();
|
|
1340
|
+
const out = new Uint8Array(PAIRING_VERIFICATION_SECRET_BYTES);
|
|
1341
|
+
let buffer = 0;
|
|
1342
|
+
let bits = 0;
|
|
1343
|
+
let index = 0;
|
|
1344
|
+
for (const char of upper) {
|
|
1345
|
+
buffer = buffer << 5 | BASE32_ALPHABET.indexOf(char);
|
|
1346
|
+
bits += 5;
|
|
1347
|
+
if (bits >= 8) {
|
|
1348
|
+
bits -= 8;
|
|
1349
|
+
out[index++] = buffer >>> bits & 255;
|
|
1350
|
+
}
|
|
1351
|
+
buffer &= (1 << bits) - 1;
|
|
1352
|
+
}
|
|
1353
|
+
if (buffer !== 0) throw invalidInput("Pairing verification secret has nonzero trailing bits");
|
|
1354
|
+
return out;
|
|
1355
|
+
}
|
|
1356
|
+
function formatPairingInput(pairingCode, pv) {
|
|
1357
|
+
if (pairingCode.length === 0) throw invalidInput("Pairing code must not be empty");
|
|
1358
|
+
return `${pairingCode}.${encodePairingVerificationSecret(pv)}`;
|
|
1359
|
+
}
|
|
1360
|
+
function parsePairingInput(text) {
|
|
1361
|
+
const dot = text.lastIndexOf(".");
|
|
1362
|
+
if (dot <= 0) throw invalidInput("Pairing input must be <pairing code>.<verification secret>");
|
|
1363
|
+
return { pairingCode: text.slice(0, dot), pv: decodePairingVerificationSecret(text.slice(dot + 1)) };
|
|
1364
|
+
}
|
|
1365
|
+
var DEVICE_PAIRING_MAC_DOMAIN = "byok-pairing-device-mac-v1\n";
|
|
1366
|
+
var CLIENT_PAIRING_MAC_DOMAIN = "byok-pairing-client-mac-v1\n";
|
|
1367
|
+
var PairingMacSchema = base64UrlBytes(32);
|
|
1368
|
+
var ChainHeadSchema = z.strictObject({
|
|
1369
|
+
seq: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
1370
|
+
hash: z.string().regex(CONTENT_HASH_PATTERN)
|
|
1371
|
+
});
|
|
1372
|
+
var DeviceMacInputSchema = z.object({ tenantId: z.string().min(1), deviceId: z.string().min(1), member: MemberKeyBundleV1Schema });
|
|
1373
|
+
var ClientMacInputSchema = z.object({ member: MemberKeyBundleV1Schema, chainHead: ChainHeadSchema });
|
|
1374
|
+
function devicePairingMacInput(input) {
|
|
1375
|
+
const parsed = safeParseTotal(DeviceMacInputSchema, input);
|
|
1376
|
+
if (!parsed.success || parsed.data.member.deviceId !== parsed.data.deviceId) {
|
|
1377
|
+
throw invalidInput("Device pairing MAC input must name the device member's own device id");
|
|
1378
|
+
}
|
|
1379
|
+
const { tenantId, deviceId, member } = parsed.data;
|
|
1380
|
+
return concat2(utf82(DEVICE_PAIRING_MAC_DOMAIN), canonicalizeJsonBytes({ tenantId, deviceId, member }));
|
|
1381
|
+
}
|
|
1382
|
+
function clientPairingMacInput(input) {
|
|
1383
|
+
const parsed = safeParseTotal(ClientMacInputSchema, input);
|
|
1384
|
+
if (!parsed.success || parsed.data.member.deviceId !== void 0) {
|
|
1385
|
+
throw invalidInput("Client pairing MAC input must hold a client member and a chain head");
|
|
1386
|
+
}
|
|
1387
|
+
const { member, chainHead } = parsed.data;
|
|
1388
|
+
return concat2(utf82(CLIENT_PAIRING_MAC_DOMAIN), canonicalizeJsonBytes({ member, chainHead: { seq: chainHead.seq, hash: chainHead.hash } }));
|
|
1389
|
+
}
|
|
1390
|
+
function checkPv(pv) {
|
|
1391
|
+
if (pv.byteLength !== PAIRING_VERIFICATION_SECRET_BYTES) {
|
|
1392
|
+
throw invalidInput(`Pairing verification secret must be ${PAIRING_VERIFICATION_SECRET_BYTES} bytes`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
async function computeDevicePairingMac(pv, input) {
|
|
1396
|
+
checkPv(pv);
|
|
1397
|
+
return encodeBase64Url(await hmacSha2562(pv, devicePairingMacInput(input)));
|
|
1398
|
+
}
|
|
1399
|
+
async function computeClientPairingMac(pv, input) {
|
|
1400
|
+
checkPv(pv);
|
|
1401
|
+
return encodeBase64Url(await hmacSha2562(pv, clientPairingMacInput(input)));
|
|
1402
|
+
}
|
|
1403
|
+
async function verifyDevicePairingMac(pv, input, mac) {
|
|
1404
|
+
checkPv(pv);
|
|
1405
|
+
return hmacVerify(pv, () => devicePairingMacInput(input), mac);
|
|
1406
|
+
}
|
|
1407
|
+
async function verifyClientPairingMac(pv, input, mac) {
|
|
1408
|
+
checkPv(pv);
|
|
1409
|
+
return hmacVerify(pv, () => clientPairingMacInput(input), mac);
|
|
1410
|
+
}
|
|
1411
|
+
async function hmacVerify(key, buildData, mac) {
|
|
1412
|
+
const parsed = safeParseTotal(PairingMacSchema, mac);
|
|
1413
|
+
if (!parsed.success) return false;
|
|
1414
|
+
let data;
|
|
1415
|
+
try {
|
|
1416
|
+
data = buildData();
|
|
1417
|
+
} catch {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
const subtle = webCryptoSubtle();
|
|
1421
|
+
const hmacKey = await subtle.importKey("raw", copy2(key), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
1422
|
+
return subtle.verify("HMAC", hmacKey, decodeBase64Url(parsed.data, "mac"), data);
|
|
1423
|
+
}
|
|
1424
|
+
var MemberIdSchema = z.string().regex(MEMBER_ID_PATTERN);
|
|
1425
|
+
var ScopeIdSchema = z.string().min(1);
|
|
1426
|
+
var AddSubjectSchema = z.strictObject({ member: z.unknown() });
|
|
1427
|
+
var RemoveSubjectSchema = z.strictObject({ memberId: MemberIdSchema });
|
|
1428
|
+
var RotateSubjectSchema = z.strictObject({
|
|
1429
|
+
memberId: MemberIdSchema,
|
|
1430
|
+
kemPublicJwk: P256PublicJwkSchema,
|
|
1431
|
+
kemEpoch: KemEpochSchema,
|
|
1432
|
+
kemSig: KemSigSchema
|
|
1433
|
+
});
|
|
1434
|
+
var ScopePolicySubjectSchema = z.strictObject({
|
|
1435
|
+
scopeId: ScopeIdSchema,
|
|
1436
|
+
categories: z.array(z.enum(SEALED_CATEGORIES)).min(1),
|
|
1437
|
+
members: z.array(MemberIdSchema).min(1)
|
|
1438
|
+
});
|
|
1439
|
+
var ShredSubjectSchema = z.strictObject({ scopeId: ScopeIdSchema });
|
|
1440
|
+
var SignerSchema = z.object({ signer: MemberIdSchema });
|
|
1441
|
+
var GenesisSchema = z.object({ op: z.literal("add"), subject: AddSubjectSchema });
|
|
1442
|
+
var HeadSchema = z.object({ seq: z.number(), hash: z.string() });
|
|
1443
|
+
var EntriesSchema = z.array(z.unknown());
|
|
1444
|
+
var AnchorInputSchema = z.object({
|
|
1445
|
+
tenantId: z.string(),
|
|
1446
|
+
entries: EntriesSchema,
|
|
1447
|
+
self: z.unknown(),
|
|
1448
|
+
approverSigningPublicJwk: P256PublicJwkSchema,
|
|
1449
|
+
authenticatedHead: z.unknown().optional()
|
|
1450
|
+
});
|
|
1451
|
+
function emptyMemberChainState(tenantId) {
|
|
1452
|
+
return { tenantId, head: null, members: /* @__PURE__ */ new Map(), removed: /* @__PURE__ */ new Set(), scopes: /* @__PURE__ */ new Map(), shredded: /* @__PURE__ */ new Set() };
|
|
1453
|
+
}
|
|
1454
|
+
function memberChainScopeRecipients(state, scopeId) {
|
|
1455
|
+
const scope = state.scopes.get(scopeId);
|
|
1456
|
+
if (scope === void 0) return [];
|
|
1457
|
+
return scope.members.flatMap((memberId) => {
|
|
1458
|
+
const member = state.members.get(memberId);
|
|
1459
|
+
return member === void 0 ? [] : [member];
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
async function applyMemberChainEntries(state, entries) {
|
|
1463
|
+
const list = safeParseTotal(EntriesSchema, entries);
|
|
1464
|
+
if (!list.success) return { ok: false, reason: "member_chain_entry_invalid", index: 0 };
|
|
1465
|
+
const result = await walk(state, list.data, () => false);
|
|
1466
|
+
return result.ok ? { ok: true, state: result.state } : result;
|
|
1467
|
+
}
|
|
1468
|
+
async function genesisSigner(state, raw, signer) {
|
|
1469
|
+
if (state.head !== null || state.members.size > 0) return void 0;
|
|
1470
|
+
const entry = safeParseTotal(GenesisSchema, raw);
|
|
1471
|
+
const member = entry.success ? await parseMemberKeyBundle(entry.data.subject.member) : void 0;
|
|
1472
|
+
return member !== void 0 && member.memberId === signer ? member : void 0;
|
|
1473
|
+
}
|
|
1474
|
+
async function walk(state, entries, mark) {
|
|
1475
|
+
const next = {
|
|
1476
|
+
tenantId: state.tenantId,
|
|
1477
|
+
head: state.head,
|
|
1478
|
+
members: new Map(state.members),
|
|
1479
|
+
removed: new Set(state.removed),
|
|
1480
|
+
scopes: new Map(state.scopes),
|
|
1481
|
+
shredded: new Set(state.shredded)
|
|
1482
|
+
};
|
|
1483
|
+
let marked;
|
|
1484
|
+
for (const [index, raw] of entries.entries()) {
|
|
1485
|
+
const signer = safeParseTotal(SignerSchema, raw);
|
|
1486
|
+
if (!signer.success) return { ok: false, reason: "member_chain_entry_invalid", index };
|
|
1487
|
+
const signerBundle = next.members.get(signer.data.signer) ?? await genesisSigner(next, raw, signer.data.signer);
|
|
1488
|
+
if (signerBundle === void 0) return { ok: false, reason: "member_chain_signer_invalid", index };
|
|
1489
|
+
const verified = await verifyMemberChainEntry(raw, signerBundle.signingPublicJwk);
|
|
1490
|
+
if (!verified.ok) return { ok: false, reason: verified.reason, index };
|
|
1491
|
+
const entry = verified.entry;
|
|
1492
|
+
const expected = next.head === null ? { seq: 1, prev: null } : { seq: next.head.seq + 1, prev: next.head.hash };
|
|
1493
|
+
if (entry.tenantId !== next.tenantId || entry.seq !== expected.seq || entry.prev !== expected.prev) {
|
|
1494
|
+
return { ok: false, reason: "member_chain_link_invalid", index };
|
|
1495
|
+
}
|
|
1496
|
+
if (marked === void 0 && mark(entry, signerBundle.signingPublicJwk)) marked = { index, seq: entry.seq, hash: verified.hash };
|
|
1497
|
+
const applied = await applySubject(next, entry);
|
|
1498
|
+
if (applied !== true) return { ok: false, reason: applied === false ? "member_chain_subject_invalid" : applied, index };
|
|
1499
|
+
next.head = { seq: entry.seq, hash: verified.hash };
|
|
1500
|
+
}
|
|
1501
|
+
return { ok: true, state: next, marked };
|
|
1502
|
+
}
|
|
1503
|
+
async function applySubject(state, entry) {
|
|
1504
|
+
switch (entry.op) {
|
|
1505
|
+
case "add": {
|
|
1506
|
+
const subject = safeParseTotal(AddSubjectSchema, entry.subject);
|
|
1507
|
+
const member = subject.success ? await parseMemberKeyBundle(subject.data.member) : void 0;
|
|
1508
|
+
if (member === void 0 || state.members.has(member.memberId) || state.removed.has(member.memberId)) return false;
|
|
1509
|
+
state.members.set(member.memberId, member);
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
case "remove": {
|
|
1513
|
+
const subject = safeParseTotal(RemoveSubjectSchema, entry.subject);
|
|
1514
|
+
if (!subject.success || !state.members.has(subject.data.memberId) || state.members.size === 1) return false;
|
|
1515
|
+
state.members.delete(subject.data.memberId);
|
|
1516
|
+
state.removed.add(subject.data.memberId);
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
case "rotate": {
|
|
1520
|
+
const subject = safeParseTotal(RotateSubjectSchema, entry.subject);
|
|
1521
|
+
if (!subject.success || subject.data.memberId !== entry.signer) return false;
|
|
1522
|
+
const member = state.members.get(subject.data.memberId);
|
|
1523
|
+
if (member === void 0 || subject.data.kemEpoch !== member.kemEpoch + 1) return false;
|
|
1524
|
+
const { kemPublicJwk } = subject.data;
|
|
1525
|
+
if (kemPublicJwk.x === member.kemPublicJwk.x && kemPublicJwk.y === member.kemPublicJwk.y) return false;
|
|
1526
|
+
if (!await verifyMemberKem(member, subject.data)) return false;
|
|
1527
|
+
state.members.set(member.memberId, {
|
|
1528
|
+
...member,
|
|
1529
|
+
kemPublicJwk: subject.data.kemPublicJwk,
|
|
1530
|
+
kemEpoch: subject.data.kemEpoch,
|
|
1531
|
+
kemSig: subject.data.kemSig
|
|
1532
|
+
});
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
case "scope-policy": {
|
|
1536
|
+
const subject = safeParseTotal(ScopePolicySubjectSchema, entry.subject);
|
|
1537
|
+
if (!subject.success || state.shredded.has(subject.data.scopeId)) return false;
|
|
1538
|
+
const { categories, members } = subject.data;
|
|
1539
|
+
if (!strictlyIncreasing(categories.map((category) => SEALED_CATEGORIES.indexOf(category)))) return false;
|
|
1540
|
+
if (!strictlyIncreasing(members) || !members.every((memberId) => state.members.has(memberId))) return false;
|
|
1541
|
+
if (!(state.scopes.get(subject.data.scopeId)?.members ?? members).includes(entry.signer)) return "member_chain_signer_invalid";
|
|
1542
|
+
state.scopes.set(subject.data.scopeId, { categories, members, policySeq: entry.seq });
|
|
1543
|
+
return true;
|
|
1544
|
+
}
|
|
1545
|
+
case "shred": {
|
|
1546
|
+
const subject = safeParseTotal(ShredSubjectSchema, entry.subject);
|
|
1547
|
+
if (!subject.success) return false;
|
|
1548
|
+
const scope = state.scopes.get(subject.data.scopeId);
|
|
1549
|
+
if (scope === void 0) return false;
|
|
1550
|
+
if (!scope.members.includes(entry.signer)) return "member_chain_signer_invalid";
|
|
1551
|
+
state.scopes.delete(subject.data.scopeId);
|
|
1552
|
+
state.shredded.add(subject.data.scopeId);
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function strictlyIncreasing(values) {
|
|
1558
|
+
return values.every((value, index) => index === 0 || values[index - 1] < value);
|
|
1559
|
+
}
|
|
1560
|
+
async function verifyMemberChainFromAnchor(input) {
|
|
1561
|
+
const parsed = safeParseTotal(AnchorInputSchema, input);
|
|
1562
|
+
if (!parsed.success) return { ok: false, reason: "member_chain_anchor_missing" };
|
|
1563
|
+
const { tenantId, entries, self, approverSigningPublicJwk: approver, authenticatedHead } = parsed.data;
|
|
1564
|
+
let approverId;
|
|
1565
|
+
let selfText;
|
|
1566
|
+
try {
|
|
1567
|
+
approverId = await deriveMemberId(approver);
|
|
1568
|
+
selfText = canonicalizeJson(self);
|
|
1569
|
+
} catch {
|
|
1570
|
+
return { ok: false, reason: "member_chain_anchor_missing" };
|
|
1571
|
+
}
|
|
1572
|
+
const isAnchor = (entry, signerKey) => {
|
|
1573
|
+
if (entry.op !== "add" || entry.signer !== approverId || signerKey.x !== approver.x || signerKey.y !== approver.y) return false;
|
|
1574
|
+
try {
|
|
1575
|
+
return canonicalizeJson(entry.subject.member) === selfText;
|
|
1576
|
+
} catch {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
const outcome = await walk(emptyMemberChainState(tenantId), entries, isAnchor);
|
|
1581
|
+
if (!outcome.ok) return outcome;
|
|
1582
|
+
const anchor = outcome.marked;
|
|
1583
|
+
if (anchor === void 0) return { ok: false, reason: "member_chain_anchor_missing" };
|
|
1584
|
+
if (authenticatedHead !== void 0) {
|
|
1585
|
+
const head = safeParseTotal(HeadSchema, authenticatedHead);
|
|
1586
|
+
if (!head.success || head.data.seq !== anchor.seq || head.data.hash !== anchor.hash) {
|
|
1587
|
+
return { ok: false, reason: "member_chain_head_mismatch", index: anchor.index };
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return { ok: true, state: outcome.state, anchorSeq: anchor.seq };
|
|
1591
|
+
}
|
|
1045
1592
|
|
|
1046
|
-
export { MEMBER_CHAIN_OPS, SEALED_CATEGORIES, SEALED_CONTENT_SUITE, SEALED_CONTENT_VERSION, SEALED_DIRECTIONS, SealedContentHeaderV1Schema, deriveMemberId, deriveScopeKeyId, generateScopeKey, memberChainEntryHash, openScopeKeyPackage, openSealedContent, sealContent, sealScopeKeyPackage, signMemberChainEntry, verifyMemberChainEntry };
|
|
1593
|
+
export { CLIENT_PAIRING_MAC_DOMAIN, DEVICE_PAIRING_MAC_DOMAIN, MEMBER_CHAIN_OPS, MEMBER_FINGERPRINT_DOMAIN, MEMBER_KEY_BUNDLE_VERSION, PAIRING_VERIFICATION_SECRET_BYTES, SEALED_CATEGORIES, SEALED_CONTENT_SUITE, SEALED_CONTENT_VERSION, SEALED_DIRECTIONS, SealedContentHeaderV1Schema, applyMemberChainEntries, beginMemberKemRotation, computeClientPairingMac, computeDevicePairingMac, createMemberKeyBundle, decodePairingVerificationSecret, deriveMemberId, deriveScopeKeyId, emptyMemberChainState, encodePairingVerificationSecret, finishMemberKemRotation, formatPairingInput, generateMemberKeyRecord, generatePairingVerificationSecret, generateScopeKey, memberApprovalCode, memberChainEntryHash, memberChainScopeRecipients, memberFingerprint, openMemberKeyRecord, openScopeKeyPackage, openSealedContent, parseMemberKeyBundle, parsePairingInput, sealContent, sealScopeKeyPackage, signMemberChainEntry, verifyClientPairingMac, verifyDevicePairingMac, verifyMemberChainEntry, verifyMemberChainFromAnchor, verifyMemberKem };
|
|
1047
1594
|
//# sourceMappingURL=index.js.map
|
|
1048
1595
|
//# sourceMappingURL=index.js.map
|