@ibgib/core-gib 0.1.63 → 0.1.65

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.
Files changed (32) hide show
  1. package/dist/keystone/keystone-constants.d.mts +1 -0
  2. package/dist/keystone/keystone-constants.d.mts.map +1 -1
  3. package/dist/keystone/keystone-constants.mjs +1 -0
  4. package/dist/keystone/keystone-constants.mjs.map +1 -1
  5. package/dist/keystone/keystone-helpers.d.mts +27 -1
  6. package/dist/keystone/keystone-helpers.d.mts.map +1 -1
  7. package/dist/keystone/keystone-helpers.mjs +261 -16
  8. package/dist/keystone/keystone-helpers.mjs.map +1 -1
  9. package/dist/keystone/keystone-service-v1.d.mts +113 -1
  10. package/dist/keystone/keystone-service-v1.d.mts.map +1 -1
  11. package/dist/keystone/keystone-service-v1.mjs +442 -27
  12. package/dist/keystone/keystone-service-v1.mjs.map +1 -1
  13. package/dist/keystone/keystone-service-v1.respec.mjs +641 -47
  14. package/dist/keystone/keystone-service-v1.respec.mjs.map +1 -1
  15. package/dist/keystone/keystone-types.d.mts +31 -2
  16. package/dist/keystone/keystone-types.d.mts.map +1 -1
  17. package/dist/keystone/keystone-types.mjs +15 -0
  18. package/dist/keystone/keystone-types.mjs.map +1 -1
  19. package/dist/keystone/policy/profiles/profile-domain.json +2 -61
  20. package/dist/keystone/policy/profiles/profile-sync.json +2 -16
  21. package/dist/keystone/policy/schemas/keystone.high.schema.json +1 -1
  22. package/dist/keystone/policy/schemas/keystone.medium.schema.json +1 -1
  23. package/package.json +2 -2
  24. package/src/keystone/keystone-constants.mts +1 -0
  25. package/src/keystone/keystone-helpers.mts +308 -15
  26. package/src/keystone/keystone-service-v1.mts +535 -28
  27. package/src/keystone/keystone-service-v1.respec.mts +2130 -1459
  28. package/src/keystone/keystone-types.mts +50 -2
  29. package/src/keystone/policy/profiles/profile-domain.json +3 -62
  30. package/src/keystone/policy/profiles/profile-sync.json +2 -16
  31. package/src/keystone/policy/schemas/keystone.high.schema.json +1 -1
  32. package/src/keystone/policy/schemas/keystone.medium.schema.json +1 -1
@@ -1,4 +1,4 @@
1
- import { extractErrorMsg, hash, HashAlgorithm, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
1
+ import { extractErrorMsg, hash, HashAlgorithm, pretty, getUUID } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
2
2
  import { GIB } from "@ibgib/ts-gib/dist/V1/constants.mjs";
3
3
  import { Ib, IbGibAddr, TransformResult } from "@ibgib/ts-gib/dist/types.mjs";
4
4
  import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
@@ -8,7 +8,7 @@ import { Factory_V1 } from "@ibgib/ts-gib/dist/V1/factory.mjs";
8
8
  import { getGib } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
9
9
 
10
10
  import { GLOBAL_LOG_A_LOT } from "../core-constants.mjs";
11
- import { KEYSTONE_ATOM, KEYSTONE_HASH_MAX_ROUNDS, KEYSTONE_POOL_ID_REGEXP, KEYSTONE_SALT_REGEXP, KEYSTONE_USERNAME_REGEXP, KEYSTONE_DESCRIPTION_REGEXP, KEYSTONE_VERB_MANAGE } from "./keystone-constants.mjs";
11
+ import { KEYSTONE_ATOM, KEYSTONE_HASH_MAX_ROUNDS, KEYSTONE_POOL_ID_REGEXP, KEYSTONE_SALT_REGEXP, KEYSTONE_USERNAME_REGEXP, KEYSTONE_DESCRIPTION_REGEXP, KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE } from "./keystone-constants.mjs";
12
12
  import {
13
13
  KeystoneData_V1, KeystoneIbGib_V1, KeystoneIb_V1, KeystoneChallengePool,
14
14
  DeterministicResult, KeystoneProof, KeystonePoolConfig,
@@ -18,6 +18,11 @@ import {
18
18
  KEYSTONE_CHALLENGE_TYPE_VALID_VALUES,
19
19
  KeystonePoolConfig_HashV1,
20
20
  DelegateKeystoneInfo,
21
+ ClaimDetails,
22
+ ClaimDetails_AddPool,
23
+ ClaimDetails_RemovePool,
24
+ ClaimDetails_ReplacePool,
25
+ ClaimDetails_ChangePassword
21
26
  } from "./keystone-types.mjs";
22
27
  import { MetaspaceService } from "../witness/space/metaspace/metaspace-types.mjs";
23
28
  import { IbGibSpaceAny } from "../witness/space/space-base-v1.mjs";
@@ -360,6 +365,7 @@ export async function applyReplenishmentStrategy({
360
365
  masterSecret,
361
366
  strategy,
362
367
  config,
368
+ verb,
363
369
  }: {
364
370
  prevPools: KeystoneChallengePool[],
365
371
  targetPoolId: string,
@@ -370,6 +376,7 @@ export async function applyReplenishmentStrategy({
370
376
  */
371
377
  strategy: any, // Typed as 'any' or 'KeystoneStrategyAny' to avoid circular dep if possible
372
378
  config: KeystonePoolConfig,
379
+ verb?: string,
373
380
  }): Promise<KeystoneChallengePool[]> {
374
381
  const lc = `[applyReplenishmentStrategy]`;
375
382
  try {
@@ -381,7 +388,10 @@ export async function applyReplenishmentStrategy({
381
388
  const poolSecret = await strategy.derivePoolSecret({ masterSecret });
382
389
  const timestamp = Date.now().toString();
383
390
 
384
- const strategyType = config.behavior.replenish;
391
+ let strategyType = config.behavior.replenish;
392
+ if (verb === KEYSTONE_VERB_REVOKE) {
393
+ strategyType = KeystoneReplenishStrategy.deleteAll;
394
+ }
385
395
 
386
396
  if (strategyType === KeystoneReplenishStrategy.topUp) {
387
397
  // Remove consumed
@@ -399,17 +409,10 @@ export async function applyReplenishmentStrategy({
399
409
  pool.challenges[newId] = await strategy.generateChallenge({ solution });
400
410
  }
401
411
  } else if (strategyType === KeystoneReplenishStrategy.replaceAll) {
402
- pool.challenges = {};
403
-
404
- for (let i = 0; i < config.behavior.size; i++) {
405
- const newId = await generateOpaqueChallengeId({
406
- salt: config.salt, timestamp, index: i
407
- });
408
- const solution = await strategy.generateSolution({
409
- poolSecret, poolId: pool.id, challengeId: newId
410
- });
411
- pool.challenges[newId] = await strategy.generateChallenge({ solution });
412
- }
412
+ pool.challenges = await generatePoolChallenges({
413
+ config,
414
+ masterSecret
415
+ });
413
416
  } else if (strategyType === KeystoneReplenishStrategy.consume) {
414
417
  consumedIds.forEach(id => delete pool.challenges[id]);
415
418
  } else if (strategyType === KeystoneReplenishStrategy.deleteAll) {
@@ -466,6 +469,13 @@ export async function solveAndReplenish({
466
469
 
467
470
  const strategy = KeystoneStrategyFactory.create({ config: pool.config });
468
471
  const poolSecret = await strategy.derivePoolSecret({ masterSecret });
472
+
473
+ // Verify secret correctness before solving
474
+ const isSecretCorrect = await verifyPoolSecret({ pool, signingSecret: masterSecret });
475
+ if (!isSecretCorrect) {
476
+ throw new Error(`Crypto Violation: Invalid signing secret for pool: ${pool.id} (E: 8a4c2b1d3e5f6a7b8c9d0e1f2a3b4c5f)`);
477
+ }
478
+
469
479
  const solutions: KeystoneSolution[] = [];
470
480
 
471
481
  // 1. Solve
@@ -490,7 +500,8 @@ export async function solveAndReplenish({
490
500
  consumedIds: challengeIds,
491
501
  masterSecret,
492
502
  strategy,
493
- config: pool.config
503
+ config: pool.config,
504
+ verb: claim.verb
494
505
  });
495
506
 
496
507
  return { proof, nextPools };
@@ -873,6 +884,190 @@ export async function validateKeystoneTransition({
873
884
  }
874
885
  }
875
886
 
887
+ // Claim details verification (transition validation)
888
+ const signingPoolIds = new Set(currData.proofs.map(p => p.solutions?.[0]?.poolId).filter(Boolean));
889
+
890
+ for (const proof of currData.proofs) {
891
+ const claim = proof.claim;
892
+ if (claim?.details) {
893
+ const details = claim.details;
894
+ const type = details.type;
895
+ const info = details.info;
896
+
897
+ if (type === 'add-pool') {
898
+ // Cast info to ClaimDetails_AddPool
899
+ const addInfo = info as ClaimDetails_AddPool;
900
+ if (!addInfo || !Array.isArray(addInfo.add) || addInfo.add.length === 0) {
901
+ errors.push(`details type is '${type}' but payload is invalid. (E: 1fa2a8cb28c9b60e68994511a5825)`);
902
+ continue;
903
+ }
904
+
905
+ // Verify that the set of pools in current equals previous plus the added pools
906
+ const prevIds = new Set(prevData.challengePools.map(p => p.id));
907
+ const currIds = new Set(currData.challengePools.map(p => p.id));
908
+
909
+ // All added pools must be in current
910
+ for (const id of addInfo.add) {
911
+ if (!currIds.has(id)) {
912
+ errors.push(`ClaimDetails_AddPool lists pool ${id} but it is missing in the evolved keystone. (E: 2fa2a8cb28c9b60e68994511a5825)`);
913
+ }
914
+ if (prevIds.has(id)) {
915
+ errors.push(`ClaimDetails_AddPool attempts to add existing pool ${id}. (E: 3fa2a8cb28c9b60e68994511a5825)`);
916
+ }
917
+ }
918
+
919
+ // Verify size
920
+ if (currData.challengePools.length !== prevData.challengePools.length + addInfo.add.length) {
921
+ errors.push(`Structural mismatch: expected ${prevData.challengePools.length + addInfo.add.length} pools, found ${currData.challengePools.length}. (E: 4fa2a8cb28c9b60e68994511a5825)`);
922
+ }
923
+
924
+ // Verify all previous pools are unmodified
925
+ for (const prevPool of prevData.challengePools) {
926
+ const currPool = currData.challengePools.find(p => p.id === prevPool.id);
927
+ if (!currPool) {
928
+ errors.push(`Existing pool ${prevPool.id} was removed during add-pool transition. (E: 5fa2a8cb28c9b60e68994511a5825)`);
929
+ continue;
930
+ }
931
+ // Verify configuration did not change
932
+ if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
933
+ errors.push(`Existing pool config ${prevPool.id} was mutated during add-pool transition. (E: 6fa2a8cb28c9b60e68994511a5825)`);
934
+ }
935
+ // Verify challenges did not change, unless this was the signing pool used to authorize the transition
936
+ if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
937
+ errors.push(`Existing pool challenges ${prevPool.id} were mutated during add-pool transition. (E: 7fa2a8cb28c9b60e68994511a5825)`);
938
+ }
939
+ }
940
+
941
+ } else if (type === 'replace-pool') {
942
+ const replaceInfo = info as ClaimDetails_ReplacePool;
943
+ if (!replaceInfo || typeof replaceInfo.replace !== 'string' || !replaceInfo.replace) {
944
+ errors.push(`details type is '${type}' but payload is invalid. (E: 8fa2a8cb28c9b60e68994511a5825)`);
945
+ continue;
946
+ }
947
+
948
+ const targetId = replaceInfo.replace;
949
+
950
+ // Verify that the set of pool IDs in current equals previous
951
+ const prevIds = prevData.challengePools.map(p => p.id).sort();
952
+ const currIds = currData.challengePools.map(p => p.id).sort();
953
+ if (JSON.stringify(prevIds) !== JSON.stringify(currIds)) {
954
+ errors.push(`replace-pool transition mutated the pool IDs set. (E: 9fa2a8cb28c9b60e68994511a5825)`);
955
+ }
956
+
957
+ // Assert target pool was replaced (challenges modified)
958
+ const prevTarget = prevData.challengePools.find(p => p.id === targetId);
959
+ const currTarget = currData.challengePools.find(p => p.id === targetId);
960
+ if (!prevTarget || !currTarget) {
961
+ errors.push(`Replaced pool ${targetId} not found in keystone challenge pools. (E: afa2a8cb28c9b60e68994511a5825)`);
962
+ }
963
+
964
+ // Verify all other pools are completely unmodified
965
+ for (const prevPool of prevData.challengePools) {
966
+ if (prevPool.id === targetId) continue;
967
+ const currPool = currData.challengePools.find(p => p.id === prevPool.id);
968
+ if (!currPool) continue;
969
+
970
+ if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
971
+ errors.push(`Unrelated pool config ${prevPool.id} was mutated during replace-pool. (E: bfa2a8cb28c9b60e68994511a5825)`);
972
+ }
973
+ if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
974
+ errors.push(`Unrelated pool challenges ${prevPool.id} were mutated during replace-pool. (E: cfa2a8cb28c9b60e68994511a5825)`);
975
+ }
976
+ }
977
+ } else if (type === 'remove-pool') {
978
+ const removeInfo = info as ClaimDetails_RemovePool;
979
+ if (!removeInfo || !Array.isArray(removeInfo.remove) || removeInfo.remove.length === 0) {
980
+ errors.push(`details type is '${type}' but payload is invalid. (E: b7a2a8cb28c9b60e68994511a5825)`);
981
+ continue;
982
+ }
983
+
984
+ const removeSet = new Set(removeInfo.remove);
985
+
986
+ // Verify that the set of pool IDs in current equals previous minus the removed pools
987
+ const prevIds = new Set(prevData.challengePools.map(p => p.id));
988
+ const currIds = new Set(currData.challengePools.map(p => p.id));
989
+
990
+ for (const id of removeInfo.remove) {
991
+ if (!prevIds.has(id)) {
992
+ errors.push(`ClaimDetails_RemovePool lists pool ${id} but it is missing in the previous keystone. (E: c7a2a8cb28c9b60e68994511a5825)`);
993
+ }
994
+ if (currIds.has(id)) {
995
+ errors.push(`ClaimDetails_RemovePool lists pool ${id} but it is still present in the evolved keystone. (E: d7a2a8cb28c9b60e68994511a5825)`);
996
+ }
997
+ }
998
+
999
+ // Verify size
1000
+ if (currData.challengePools.length !== prevData.challengePools.length - removeInfo.remove.length) {
1001
+ errors.push(`Structural mismatch: expected ${prevData.challengePools.length - removeInfo.remove.length} pools, found ${currData.challengePools.length}. (E: e7a2a8cb28c9b60e68994511a5825)`);
1002
+ }
1003
+
1004
+ // Verify all remaining pools are unmodified
1005
+ for (const prevPool of prevData.challengePools) {
1006
+ if (removeSet.has(prevPool.id)) continue;
1007
+ const currPool = currData.challengePools.find(p => p.id === prevPool.id);
1008
+ if (!currPool) {
1009
+ errors.push(`Pool ${prevPool.id} was unexpectedly removed. (E: f7a2a8cb28c9b60e68994511a5825)`);
1010
+ continue;
1011
+ }
1012
+ if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
1013
+ errors.push(`Remaining pool config ${prevPool.id} was mutated during remove-pool. (E: 07a2a8cb28c9b60e68994511a5825)`);
1014
+ }
1015
+ if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
1016
+ errors.push(`Remaining pool challenges ${prevPool.id} were mutated during remove-pool. (E: 17a2a8cb28c9b60e68994511a5825)`);
1017
+ }
1018
+ }
1019
+
1020
+ } else if (type === 'change-password') {
1021
+ const changeInfo = info as ClaimDetails_ChangePassword;
1022
+ if (!changeInfo || !Array.isArray(changeInfo.change) || changeInfo.change.length === 0) {
1023
+ errors.push(`details type is '${type}' but payload is invalid. (E: dfa2a8cb28c9b60e68994511a5825)`);
1024
+ continue;
1025
+ }
1026
+
1027
+ // Verify that the set of pool IDs is unmodified
1028
+ const prevIds = prevData.challengePools.map(p => p.id).sort();
1029
+ const currIds = currData.challengePools.map(p => p.id).sort();
1030
+ if (JSON.stringify(prevIds) !== JSON.stringify(currIds)) {
1031
+ errors.push(`change-password transition mutated the pool IDs set. (E: efa2a8cb28c9b60e68994511a5825)`);
1032
+ }
1033
+
1034
+ const rotatedIds = new Set(changeInfo.change);
1035
+
1036
+ for (const prevPool of prevData.challengePools) {
1037
+ const currPool = currData.challengePools.find(p => p.id === prevPool.id);
1038
+ if (!currPool) continue;
1039
+
1040
+ if (rotatedIds.has(prevPool.id)) {
1041
+ // Check: rotated pool must not be foreign!
1042
+ if (prevPool.isForeign) {
1043
+ errors.push(`Security Violation: Password rotation attempted on foreign pool ${prevPool.id}. (E: ffa2a8cb28c9b60e68994511a5825)`);
1044
+ }
1045
+ // Config must be identical except for the salt
1046
+ const prevConfCopy = { ...prevPool.config, salt: '' };
1047
+ const currConfCopy = { ...currPool.config, salt: '' };
1048
+ if (JSON.stringify(prevConfCopy) !== JSON.stringify(currConfCopy)) {
1049
+ errors.push(`Pool config metadata for rotated pool ${prevPool.id} was mutated. (E: 00b2a8cb28c9b60e68994511a5825)`);
1050
+ }
1051
+ // Salt must be different
1052
+ if (prevPool.config.salt === currPool.config.salt) {
1053
+ errors.push(`Pool salt for rotated pool ${prevPool.id} was not rotated. (E: 01b2a8cb28c9b60e68994511a5825)`);
1054
+ }
1055
+ } else {
1056
+ // Non-rotated pools must be completely unmodified
1057
+ if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
1058
+ errors.push(`Unrelated pool config ${prevPool.id} was mutated during change-password. (E: 02b2a8cb28c9b60e68994511a5825)`);
1059
+ }
1060
+ if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
1061
+ errors.push(`Unrelated pool challenges ${prevPool.id} were mutated during change-password. (E: 03b2a8cb28c9b60e68994511a5825)`);
1062
+ }
1063
+ }
1064
+ }
1065
+ } else {
1066
+ errors.push(`Unknown claim details type: ${type}`);
1067
+ }
1068
+ }
1069
+ }
1070
+
876
1071
  return errors;
877
1072
  } catch (error) {
878
1073
  console.error(`${lc} ${extractErrorMsg(error)}`);
@@ -1280,4 +1475,102 @@ export async function getIdentity_throwIfUndefined({
1280
1475
  const identityIbGib = await getIdentity({ metaspace, addr, space });
1281
1476
  if (!identityIbGib) { throw new Error(`addr (${addr}) not found in space: ${space.ib} (E: 7533682e805819cc78bdb0d8960be826)`); }
1282
1477
  return identityIbGib;
1478
+ }
1479
+
1480
+ /**
1481
+ * Pure helper to generate challenges map for a pool configuration using a masterSecret.
1482
+ */
1483
+ export async function generatePoolChallenges({
1484
+ config,
1485
+ masterSecret
1486
+ }: {
1487
+ config: KeystonePoolConfig;
1488
+ masterSecret: string;
1489
+ }): Promise<{ [challengeId: string]: any }> {
1490
+ const strategy = KeystoneStrategyFactory.create({ config });
1491
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret });
1492
+ const challenges: { [challengeId: string]: any } = {};
1493
+ const targetSize = config.behavior.size;
1494
+ const timestamp = Date.now().toString();
1495
+
1496
+ for (let i = 0; i < targetSize; i++) {
1497
+ const challengeId = await generateOpaqueChallengeId({
1498
+ salt: config.salt,
1499
+ timestamp,
1500
+ index: i
1501
+ });
1502
+ const solution = await strategy.generateSolution({
1503
+ poolSecret,
1504
+ poolId: config.id,
1505
+ challengeId
1506
+ });
1507
+ challenges[challengeId] = await strategy.generateChallenge({ solution });
1508
+ }
1509
+ return challenges;
1510
+ }
1511
+
1512
+ /**
1513
+ * Pure helper to rotate a pool to a new masterSecret.
1514
+ * Takes the previous pool config, generates a new salt, and builds a new pool with refreshed challenges.
1515
+ */
1516
+ export async function rotatePoolChallenges({
1517
+ prevPool,
1518
+ newMasterSecret
1519
+ }: {
1520
+ prevPool: KeystoneChallengePool;
1521
+ newMasterSecret: string;
1522
+ }): Promise<KeystoneChallengePool> {
1523
+ const prevConfig = prevPool.config;
1524
+ const newSalt = (await getUUID()).substring(0, 16);
1525
+
1526
+ // Copy the config exactly, updating only the salt
1527
+ const newConfig: KeystonePoolConfig = {
1528
+ ...prevConfig,
1529
+ salt: newSalt
1530
+ };
1531
+
1532
+ const newChallenges = await generatePoolChallenges({
1533
+ config: newConfig,
1534
+ masterSecret: newMasterSecret
1535
+ });
1536
+
1537
+ return {
1538
+ id: prevPool.id,
1539
+ config: newConfig,
1540
+ challenges: newChallenges,
1541
+ ...(prevPool.isForeign !== undefined ? { isForeign: prevPool.isForeign } : {}),
1542
+ ...(prevPool.metadata ? { metadata: prevPool.metadata } : {})
1543
+ };
1544
+ }
1545
+
1546
+ /**
1547
+ * Pure helper to verify if a candidate signing secret (passphrase) is correct for a specific challenge pool.
1548
+ * Does not mutate any state.
1549
+ */
1550
+ export async function verifyPoolSecret({
1551
+ pool,
1552
+ signingSecret
1553
+ }: {
1554
+ pool: KeystoneChallengePool;
1555
+ signingSecret: string;
1556
+ }): Promise<boolean> {
1557
+ const challengeIds = Object.keys(pool.challenges);
1558
+ if (challengeIds.length === 0) return false;
1559
+
1560
+ try {
1561
+ const firstId = challengeIds[0];
1562
+ const challenge = pool.challenges[firstId];
1563
+
1564
+ const strategy = KeystoneStrategyFactory.create({ config: pool.config });
1565
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret: signingSecret });
1566
+ const solution = await strategy.generateSolution({
1567
+ poolSecret,
1568
+ poolId: pool.id,
1569
+ challengeId: firstId
1570
+ });
1571
+
1572
+ return await strategy.validateSolution({ solution, challenge });
1573
+ } catch {
1574
+ return false;
1575
+ }
1283
1576
  }