@ibgib/core-gib 0.1.65 → 0.1.66

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 (21) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/keystone/keystone-service-v1.d.mts +54 -18
  3. package/dist/keystone/keystone-service-v1.d.mts.map +1 -1
  4. package/dist/keystone/keystone-service-v1.mjs +56 -21
  5. package/dist/keystone/keystone-service-v1.mjs.map +1 -1
  6. package/dist/keystone/keystone-service-v1.respec.mjs +744 -471
  7. package/dist/keystone/keystone-service-v1.respec.mjs.map +1 -1
  8. package/dist/keystone/keystone-types.d.mts +9 -1
  9. package/dist/keystone/keystone-types.d.mts.map +1 -1
  10. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.d.mts +27 -4
  11. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.d.mts.map +1 -1
  12. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs +230 -135
  13. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs.map +1 -1
  14. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs +1 -1
  15. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs.map +1 -1
  16. package/package.json +1 -1
  17. package/src/keystone/keystone-service-v1.mts +112 -32
  18. package/src/keystone/keystone-service-v1.respec.mts +791 -556
  19. package/src/keystone/keystone-types.mts +9 -1
  20. package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mts +294 -131
  21. package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mts +1 -1
@@ -2,7 +2,7 @@ import {
2
2
  respecfully, iReckon, ifWe, firstOfAll, firstOfEach, lastOfAll, lastOfEach, respecfullyDear, ifWeMight
3
3
  } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
4
4
  const maam = `[${import.meta.url}]`, sir = maam;
5
- import { clone, hash, getUUID } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
5
+ import { clone, hash, getUUID, extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
6
6
  import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
7
7
  import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
8
8
 
@@ -158,6 +158,33 @@ class MockMetaspaceService {
158
158
  const addr = getIbGibAddr({ ibGib });
159
159
  this.timelineHeads.set(tjpGib, addr);
160
160
  }
161
+
162
+ /**
163
+ * Resolves the latest address for a timeline.
164
+ */
165
+ async getLatestAddr({ addr, tjpAddr, space }: { addr?: string, tjpAddr?: string, space?: any }): Promise<string | undefined> {
166
+ const targetAddr = tjpAddr || addr;
167
+ if (!targetAddr) return undefined;
168
+ const targetSpace = space || this.space;
169
+ const gib = targetAddr.split('^')[1] || '';
170
+ let tjpGib = gib.includes('.') ? gib.split('.').slice(1).join('.') : gib;
171
+ if (this.timelineHeads.has(tjpGib)) {
172
+ return this.timelineHeads.get(tjpGib);
173
+ }
174
+ if (targetSpace && typeof targetSpace.witness === 'function') {
175
+ const res = await targetSpace.witness({
176
+ data: {
177
+ cmd: 'get',
178
+ cmdModifiers: ['latest', 'addrs'],
179
+ ibGibAddrs: [targetAddr]
180
+ }
181
+ });
182
+ if (res?.data?.success && res?.data?.latestAddrsMap?.[targetAddr]) {
183
+ return res.data.latestAddrsMap[targetAddr];
184
+ }
185
+ }
186
+ return targetAddr;
187
+ }
161
188
  }
162
189
 
163
190
  // ===========================================================================
@@ -1625,606 +1652,814 @@ await respecfully(sir, 'Suite G: Combined Pools & Password Rotation', async () =
1625
1652
  });
1626
1653
 
1627
1654
  await ifWeMight(sir, 'revokes a keystone using a separate revocation pool', async () => {
1628
- const stdSalt = "std_salt";
1629
- const revokeSalt = "revoke_salt";
1630
- const stdConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: stdSalt });
1631
- const revokeConfig = createStandardPoolConfig({ id: POOL_ID_REVOKE, salt: revokeSalt });
1632
- revokeConfig.allowedVerbs = [KEYSTONE_VERB_REVOKE];
1633
- revokeConfig.behavior.replenish = KeystoneReplenishStrategy.deleteAll;
1634
-
1635
- const keystone = await service.genesis({
1636
- masterSecret: userSecret,
1637
- configs: [stdConfig, revokeConfig],
1638
- metaspace: mockMetaspace,
1639
- space: mockSpace as any,
1640
- });
1655
+ const lc = `[revokes a keystone using a separate revocation pool]`;
1656
+ try {
1657
+ if (logalot) { console.log(`${lc} starting... (I: 8efbb3af6a483aa4c1205118bd481926)`); }
1641
1658
 
1642
- const revoked = await service.revoke({
1643
- latestKeystone: keystone,
1644
- masterSecret: userSecret,
1645
- metaspace: mockMetaspace,
1646
- space: mockSpace as any,
1647
- });
1659
+ const stdSalt = "std_salt";
1660
+ const revokeSalt = "revoke_salt";
1661
+ const stdConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: stdSalt });
1662
+ const revokeConfig = createStandardPoolConfig({ id: POOL_ID_REVOKE, salt: revokeSalt });
1663
+ revokeConfig.allowedVerbs = [KEYSTONE_VERB_REVOKE];
1664
+ revokeConfig.behavior.replenish = KeystoneReplenishStrategy.deleteAll;
1648
1665
 
1649
- iReckon(sir, revoked.data?.revocationInfo).asTo('revocationInfo present').isGonnaBeTruthy();
1650
- const revokePool = revoked.data?.challengePools.find(p => p.id === POOL_ID_REVOKE)!;
1651
- iReckon(sir, Object.keys(revokePool.challenges).length).asTo('revoke pool depleted').willEqual(0);
1666
+ const keystone = await service.genesis({
1667
+ masterSecret: userSecret,
1668
+ configs: [stdConfig, revokeConfig],
1669
+ metaspace: mockMetaspace,
1670
+ space: mockSpace as any,
1671
+ });
1672
+
1673
+ const revoked = await service.revoke({
1674
+ latestKeystone: keystone,
1675
+ masterSecret: userSecret,
1676
+ metaspace: mockMetaspace,
1677
+ space: mockSpace as any,
1678
+ });
1679
+
1680
+ iReckon(sir, revoked.data?.revocationInfo).asTo('revocationInfo present').isGonnaBeTruthy();
1681
+ const revokePool = revoked.data?.challengePools.find(p => p.id === POOL_ID_REVOKE)!;
1682
+ iReckon(sir, Object.keys(revokePool.challenges).length).asTo('revoke pool depleted').willEqual(0);
1683
+ } catch (error) {
1684
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1685
+ throw error;
1686
+ } finally {
1687
+ if (logalot) { console.log(`${lc} complete.`); }
1688
+ }
1652
1689
  });
1653
1690
 
1654
1691
  await ifWeMight(sir, 'revokes a keystone using a combined manage/revoke pool and empties it', async () => {
1655
- const salt = "combined_salt";
1656
- const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt });
1657
- manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1692
+ const lc = `[revokes a keystone using a combined manage/revoke pool and empties it]`;
1693
+ try {
1694
+ if (logalot) { console.log(`${lc} starting... (I: 31d8bc860b7cb09893ccaac3d2cf4826)`); }
1658
1695
 
1659
- const keystone = await service.genesis({
1660
- masterSecret: userSecret,
1661
- configs: [manageConfig],
1662
- metaspace: mockMetaspace,
1663
- space: mockSpace as any,
1664
- });
1696
+ const salt = "combined_salt";
1697
+ const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt });
1698
+ manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1665
1699
 
1666
- const revoked = await service.revoke({
1667
- latestKeystone: keystone,
1668
- masterSecret: userSecret,
1669
- metaspace: mockMetaspace,
1670
- space: mockSpace as any,
1671
- });
1700
+ const keystone = await service.genesis({
1701
+ masterSecret: userSecret,
1702
+ configs: [manageConfig],
1703
+ metaspace: mockMetaspace,
1704
+ space: mockSpace as any,
1705
+ });
1672
1706
 
1673
- iReckon(sir, revoked.data?.revocationInfo).asTo('revocationInfo present').isGonnaBeTruthy();
1674
- const managePool = revoked.data?.challengePools.find(p => p.id === POOL_ID_MANAGE)!;
1675
- iReckon(sir, Object.keys(managePool.challenges).length).asTo('combined manage pool depleted').willEqual(0);
1707
+ const revoked = await service.revoke({
1708
+ latestKeystone: keystone,
1709
+ masterSecret: userSecret,
1710
+ metaspace: mockMetaspace,
1711
+ space: mockSpace as any,
1712
+ });
1713
+
1714
+ iReckon(sir, revoked.data?.revocationInfo).asTo('revocationInfo present').isGonnaBeTruthy();
1715
+ const managePool = revoked.data?.challengePools.find(p => p.id === POOL_ID_MANAGE)!;
1716
+ iReckon(sir, Object.keys(managePool.challenges).length).asTo('combined manage pool depleted').willEqual(0);
1717
+ } catch (error) {
1718
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1719
+ throw error;
1720
+ } finally {
1721
+ if (logalot) { console.log(`${lc} complete.`); }
1722
+ }
1676
1723
  });
1677
1724
 
1678
1725
  await ifWeMight(sir, 'rotates user password successfully when signed by the user manage pool', async () => {
1679
- const salt = "manage_salt";
1680
- const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt });
1681
- manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1726
+ const lc = `[rotates user password successfully when signed by the user manage pool]`;
1727
+ try {
1728
+ if (logalot) { console.log(`${lc} starting... (I: 580b0bbdc7f80e35d8ee2339ac0eb526)`); }
1682
1729
 
1683
- const keystone = await service.genesis({
1684
- masterSecret: userSecret,
1685
- configs: [manageConfig],
1686
- metaspace: mockMetaspace,
1687
- space: mockSpace as any,
1688
- });
1730
+ const salt = "manage_salt";
1731
+ const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt });
1732
+ manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1689
1733
 
1690
- const newSecret = "NewUserSecret_999";
1691
- const rotated = await service.changePassword({
1692
- latestKeystone: keystone,
1693
- newMasterSecret: newSecret,
1694
- signingSecret: userSecret,
1695
- signingPoolId: POOL_ID_MANAGE,
1696
- metaspace: mockMetaspace,
1697
- space: mockSpace as any,
1698
- });
1734
+ const keystone = await service.genesis({
1735
+ masterSecret: userSecret,
1736
+ configs: [manageConfig],
1737
+ metaspace: mockMetaspace,
1738
+ space: mockSpace as any,
1739
+ });
1699
1740
 
1700
- iReckon(sir, rotated).isGonnaBeTruthy();
1741
+ const newSecret = "NewUserSecret_999";
1742
+ const { newKeystone: rotated } = await service.changePassword({
1743
+ keystoneIbGib: keystone,
1744
+ newMasterSecret: newSecret,
1745
+ signingSecret: userSecret,
1746
+ signingPoolId: POOL_ID_MANAGE,
1747
+ metaspace: mockMetaspace,
1748
+ space: mockSpace as any,
1749
+ });
1701
1750
 
1702
- // 1. Try to sign using the new secret (should succeed)
1703
- const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
1704
- const signedNew = await service.sign({
1705
- latestKeystone: rotated,
1706
- masterSecret: newSecret,
1707
- claim,
1708
- poolId: POOL_ID_MANAGE,
1709
- metaspace: mockMetaspace,
1710
- space: mockSpace as any,
1711
- });
1712
- iReckon(sir, signedNew).isGonnaBeTruthy();
1751
+ iReckon(sir, rotated).isGonnaBeTruthy();
1713
1752
 
1714
- // 2. Try to sign using the old secret (should fail)
1715
- let errorCaught = false;
1716
- try {
1717
- await service.sign({
1753
+ // 1. Try to sign using the new secret (should succeed)
1754
+ const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
1755
+ const signedNew = await service.sign({
1718
1756
  latestKeystone: rotated,
1719
- masterSecret: userSecret,
1757
+ masterSecret: newSecret,
1720
1758
  claim,
1721
1759
  poolId: POOL_ID_MANAGE,
1722
1760
  metaspace: mockMetaspace,
1723
1761
  space: mockSpace as any,
1724
1762
  });
1725
- } catch {
1726
- errorCaught = true;
1763
+ iReckon(sir, signedNew).isGonnaBeTruthy();
1764
+
1765
+ // 2. Try to sign using the old secret (should fail)
1766
+ let errorCaught = false;
1767
+ try {
1768
+ await service.sign({
1769
+ latestKeystone: rotated,
1770
+ masterSecret: userSecret,
1771
+ claim,
1772
+ poolId: POOL_ID_MANAGE,
1773
+ metaspace: mockMetaspace,
1774
+ space: mockSpace as any,
1775
+ });
1776
+ } catch {
1777
+ errorCaught = true;
1778
+ }
1779
+ iReckon(sir, errorCaught).asTo('old secret signing rejected').isGonnaBeTrue();
1780
+
1781
+ } catch (error) {
1782
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1783
+ throw error;
1784
+ } finally {
1785
+ if (logalot) { console.log(`${lc} complete.`); }
1727
1786
  }
1728
- iReckon(sir, errorCaught).asTo('old secret signing rejected').isGonnaBeTrue();
1729
1787
  });
1730
1788
 
1731
1789
  await ifWeMight(sir, 'recovers user password successfully when signed by the custodian manage pool', async () => {
1732
- const userSalt = "user_salt";
1733
- const custodianSalt = "custodian_salt";
1790
+ const lc = `[recovers user password successfully when signed by the custodian manage pool]`;
1791
+ try {
1792
+ if (logalot) { console.log(`${lc} starting... (I: 409df8ee1ae81d8b45c4a9c98e6e7626)`); }
1793
+
1794
+ const userSalt = "user_salt";
1795
+ const custodianSalt = "custodian_salt";
1796
+
1797
+ const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
1798
+ userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1799
+
1800
+ const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
1801
+ custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1802
+
1803
+ // We simulate constructing the custodian pool "offline" by the server
1804
+ const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
1805
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
1806
+ const challenges: { [id: string]: any } = {};
1807
+ for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
1808
+ const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
1809
+ const challengeId = raw.substring(0, 16);
1810
+ const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
1811
+ challenges[challengeId] = await strategy.generateChallenge({ solution });
1812
+ }
1813
+ const custodianPool: KeystoneChallengePool = {
1814
+ id: POOL_ID_CUSTODIAN_MANAGE,
1815
+ config: custodianManageConfig,
1816
+ challenges,
1817
+ isForeign: true
1818
+ };
1734
1819
 
1735
- const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
1736
- userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1820
+ const keystone = await service.genesis({
1821
+ masterSecret: userSecret,
1822
+ configs: [userManageConfig],
1823
+ metaspace: mockMetaspace,
1824
+ space: mockSpace as any,
1825
+ });
1737
1826
 
1738
- const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
1739
- custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1827
+ // Add the custodian pool to the keystone
1828
+ const keystoneWithCustodian = await service.addPools({
1829
+ latestKeystone: keystone,
1830
+ masterSecret: userSecret,
1831
+ newPools: [custodianPool],
1832
+ metaspace: mockMetaspace,
1833
+ space: mockSpace as any,
1834
+ });
1740
1835
 
1741
- // We simulate constructing the custodian pool "offline" by the server
1742
- const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
1743
- const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
1744
- const challenges: { [id: string]: any } = {};
1745
- for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
1746
- const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
1747
- const challengeId = raw.substring(0, 16);
1748
- const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
1749
- challenges[challengeId] = await strategy.generateChallenge({ solution });
1836
+ // The user forgets their secret. Recover using custodianSecret!
1837
+ const recoveredSecret = "RecoveredUserSecret_777";
1838
+ const { newKeystone: recoveredKeystone } = await service.changePassword({
1839
+ keystoneIbGib: keystoneWithCustodian,
1840
+ newMasterSecret: recoveredSecret,
1841
+ signingSecret: custodianSecret,
1842
+ signingPoolId: POOL_ID_CUSTODIAN_MANAGE,
1843
+ metaspace: mockMetaspace,
1844
+ space: mockSpace as any,
1845
+ });
1846
+
1847
+ iReckon(sir, recoveredKeystone).isGonnaBeTruthy();
1848
+
1849
+ // Verify we can sign with recovered secret
1850
+ const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
1851
+ const signedNew = await service.sign({
1852
+ latestKeystone: recoveredKeystone,
1853
+ masterSecret: recoveredSecret,
1854
+ claim,
1855
+ poolId: POOL_ID_MANAGE,
1856
+ metaspace: mockMetaspace,
1857
+ space: mockSpace as any,
1858
+ });
1859
+ iReckon(sir, signedNew).isGonnaBeTruthy();
1860
+ } catch (error) {
1861
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1862
+ throw error;
1863
+ } finally {
1864
+ if (logalot) { console.log(`${lc} complete.`); }
1750
1865
  }
1751
- const custodianPool: KeystoneChallengePool = {
1752
- id: POOL_ID_CUSTODIAN_MANAGE,
1753
- config: custodianManageConfig,
1754
- challenges,
1755
- isForeign: true
1756
- };
1866
+ });
1757
1867
 
1758
- const keystone = await service.genesis({
1759
- masterSecret: userSecret,
1760
- configs: [userManageConfig],
1761
- metaspace: mockMetaspace,
1762
- space: mockSpace as any,
1763
- });
1868
+ await ifWeMight(sir, 'rotates custodian manage pool successfully', async () => {
1869
+ const lc = `[rotates custodian manage pool successfully]`;
1870
+ try {
1871
+ if (logalot) { console.log(`${lc} starting... (I: 76d9a76dc2487949d7deb31696d3e126)`); }
1872
+
1873
+ const userSalt = "user_salt";
1874
+ const custodianSalt = "custodian_salt";
1875
+
1876
+ const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
1877
+ userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1878
+
1879
+ const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
1880
+ custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1881
+
1882
+ // Custodian Pool genesis
1883
+ const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
1884
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
1885
+ const challenges: { [id: string]: any } = {};
1886
+ for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
1887
+ const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
1888
+ const challengeId = raw.substring(0, 16);
1889
+ const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
1890
+ challenges[challengeId] = await strategy.generateChallenge({ solution });
1891
+ }
1892
+ const custodianPool: KeystoneChallengePool = {
1893
+ id: POOL_ID_CUSTODIAN_MANAGE,
1894
+ config: custodianManageConfig,
1895
+ challenges,
1896
+ isForeign: true
1897
+ };
1764
1898
 
1765
- // Add the custodian pool to the keystone
1766
- const keystoneWithCustodian = await service.addPools({
1767
- latestKeystone: keystone,
1768
- masterSecret: userSecret,
1769
- newPools: [custodianPool],
1770
- metaspace: mockMetaspace,
1771
- space: mockSpace as any,
1772
- });
1899
+ const keystone = await service.genesis({
1900
+ masterSecret: userSecret,
1901
+ configs: [userManageConfig],
1902
+ metaspace: mockMetaspace,
1903
+ space: mockSpace as any,
1904
+ });
1773
1905
 
1774
- // The user forgets their secret. Recover using custodianSecret!
1775
- const recoveredSecret = "RecoveredUserSecret_777";
1776
- const recoveredKeystone = await service.changePassword({
1777
- latestKeystone: keystoneWithCustodian,
1778
- newMasterSecret: recoveredSecret,
1779
- signingSecret: custodianSecret,
1780
- signingPoolId: POOL_ID_CUSTODIAN_MANAGE,
1781
- metaspace: mockMetaspace,
1782
- space: mockSpace as any,
1783
- });
1906
+ const keystoneWithCustodian = await service.addPools({
1907
+ latestKeystone: keystone,
1908
+ masterSecret: userSecret,
1909
+ newPools: [custodianPool],
1910
+ metaspace: mockMetaspace,
1911
+ space: mockSpace as any,
1912
+ });
1784
1913
 
1785
- iReckon(sir, recoveredKeystone).isGonnaBeTruthy();
1914
+ // Rotate custodian credentials to new secret
1915
+ const newCustodianSecret = "NewCustodianSecret_888";
1916
+ const newCustodianConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: "new_custodian_salt" });
1917
+ newCustodianConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1918
+
1919
+ const newStrategy = KeystoneStrategyFactory.create({ config: newCustodianConfig });
1920
+ const newPoolSecret = await newStrategy.derivePoolSecret({ masterSecret: newCustodianSecret });
1921
+ const newChallenges: { [id: string]: any } = {};
1922
+ for (let i = 0; i < newCustodianConfig.behavior.size; i++) {
1923
+ const raw = await hash({ s: `${newCustodianConfig.salt}${Date.now()}${i}` });
1924
+ const challengeId = raw.substring(0, 16);
1925
+ const solution = await newStrategy.generateSolution({ poolSecret: newPoolSecret, poolId: newCustodianConfig.id, challengeId });
1926
+ newChallenges[challengeId] = await newStrategy.generateChallenge({ solution });
1927
+ }
1928
+ const newCustodianPool: KeystoneChallengePool = {
1929
+ id: POOL_ID_CUSTODIAN_MANAGE,
1930
+ config: newCustodianConfig,
1931
+ challenges: newChallenges,
1932
+ isForeign: true
1933
+ };
1786
1934
 
1787
- // Verify we can sign with recovered secret
1788
- const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
1789
- const signedNew = await service.sign({
1790
- latestKeystone: recoveredKeystone,
1791
- masterSecret: recoveredSecret,
1792
- claim,
1793
- poolId: POOL_ID_MANAGE,
1794
- metaspace: mockMetaspace,
1795
- space: mockSpace as any,
1796
- });
1797
- iReckon(sir, signedNew).isGonnaBeTruthy();
1935
+ // Rotate signed by user manage pool
1936
+ const rotated = await service.replacePool({
1937
+ latestKeystone: keystoneWithCustodian,
1938
+ newPool: newCustodianPool,
1939
+ signingSecret: userSecret,
1940
+ signingPoolId: POOL_ID_MANAGE,
1941
+ metaspace: mockMetaspace,
1942
+ space: mockSpace as any,
1943
+ });
1944
+
1945
+ iReckon(sir, rotated).isGonnaBeTruthy();
1946
+
1947
+ // Verify we can sign with new custodian secret
1948
+ const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
1949
+ const signedNew = await service.sign({
1950
+ latestKeystone: rotated,
1951
+ masterSecret: newCustodianSecret,
1952
+ claim,
1953
+ poolId: POOL_ID_CUSTODIAN_MANAGE,
1954
+ metaspace: mockMetaspace,
1955
+ space: mockSpace as any,
1956
+ });
1957
+ iReckon(sir, signedNew).isGonnaBeTruthy();
1958
+
1959
+ } catch (error) {
1960
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1961
+ throw error;
1962
+ } finally {
1963
+ if (logalot) { console.log(`${lc} complete.`); }
1964
+ }
1798
1965
  });
1799
1966
 
1800
- await ifWeMight(sir, 'rotates custodian manage pool successfully', async () => {
1801
- const userSalt = "user_salt";
1802
- const custodianSalt = "custodian_salt";
1967
+ await ifWeMight(sir, 'rotates all non-foreign pools during changePassword by default', async () => {
1968
+ const lc = `[rotates all non-foreign pools during changePassword by default]`;
1969
+ try {
1970
+ if (logalot) { console.log(`${lc} starting... (I: 8449682eda6389e9d514888f9ce86f26)`); }
1971
+
1972
+ const userSalt = "user_salt";
1973
+ const custodianSalt = "custodian_salt";
1974
+ const secondarySalt = "secondary_salt";
1975
+
1976
+ const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
1977
+ userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1978
+
1979
+ // Another native/non-foreign pool
1980
+ const secondaryNativeConfig = createStandardPoolConfig({ id: "secondary-native", salt: secondarySalt });
1981
+ secondaryNativeConfig.allowedVerbs = [KEYSTONE_VERB_SYNC];
1982
+
1983
+ // A custodian/foreign pool
1984
+ const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
1985
+ custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1986
+
1987
+ // Custodian Pool genesis
1988
+ const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
1989
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
1990
+ const challenges: { [id: string]: any } = {};
1991
+ for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
1992
+ const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
1993
+ const challengeId = raw.substring(0, 16);
1994
+ const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
1995
+ challenges[challengeId] = await strategy.generateChallenge({ solution });
1996
+ }
1997
+ const custodianPool: KeystoneChallengePool = {
1998
+ id: POOL_ID_CUSTODIAN_MANAGE,
1999
+ config: custodianManageConfig,
2000
+ challenges,
2001
+ isForeign: true
2002
+ };
2003
+
2004
+ const keystone = await service.genesis({
2005
+ masterSecret: userSecret,
2006
+ configs: [userManageConfig, secondaryNativeConfig],
2007
+ metaspace: mockMetaspace,
2008
+ space: mockSpace as any,
2009
+ });
2010
+
2011
+ const keystoneWithCustodian = await service.addPools({
2012
+ latestKeystone: keystone,
2013
+ masterSecret: userSecret,
2014
+ newPools: [custodianPool],
2015
+ metaspace: mockMetaspace,
2016
+ space: mockSpace as any,
2017
+ });
2018
+
2019
+ // Rotate passphrase/password
2020
+ const newPassphrase = "SuperNewSecretPassphrase_999";
2021
+ const { newKeystone: rotated } = await service.changePassword({
2022
+ keystoneIbGib: keystoneWithCustodian,
2023
+ newMasterSecret: newPassphrase,
2024
+ signingSecret: userSecret,
2025
+ signingPoolId: POOL_ID_MANAGE,
2026
+ metaspace: mockMetaspace,
2027
+ space: mockSpace as any,
2028
+ });
1803
2029
 
1804
- const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
1805
- userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
2030
+ iReckon(sir, rotated).isGonnaBeTruthy();
1806
2031
 
1807
- const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
1808
- custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
2032
+ const data = rotated.data!;
2033
+ const rotatedManage = data.challengePools.find(p => p.id === POOL_ID_MANAGE)!;
2034
+ const rotatedSecondary = data.challengePools.find(p => p.id === "secondary-native")!;
2035
+ const rotatedCustodian = data.challengePools.find(p => p.id === POOL_ID_CUSTODIAN_MANAGE)!;
1809
2036
 
1810
- // Custodian Pool genesis
1811
- const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
1812
- const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
1813
- const challenges: { [id: string]: any } = {};
1814
- for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
1815
- const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
1816
- const challengeId = raw.substring(0, 16);
1817
- const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
1818
- challenges[challengeId] = await strategy.generateChallenge({ solution });
2037
+ // Verify native pools rotated (salt changed)
2038
+ iReckon(sir, rotatedManage.config.salt).not.willEqual(userSalt);
2039
+ iReckon(sir, rotatedSecondary.config.salt).not.willEqual(secondarySalt);
2040
+
2041
+ // Verify foreign pool was not modified (salt identical)
2042
+ iReckon(sir, rotatedCustodian.config.salt).willEqual(custodianSalt);
2043
+
2044
+ // Verify we can validate transition
2045
+ const errors = await service.validate({
2046
+ prevIbGib: keystoneWithCustodian,
2047
+ currentIbGib: rotated
2048
+ });
2049
+ iReckon(sir, errors.length).willEqual(0);
2050
+
2051
+ // Verify we can sign using the rotated 'manage' pool with the new passphrase
2052
+ const manageClaim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target_manage" };
2053
+ const signedWithNewManage = await service.sign({
2054
+ latestKeystone: rotated,
2055
+ masterSecret: newPassphrase,
2056
+ claim: manageClaim,
2057
+ poolId: POOL_ID_MANAGE,
2058
+ metaspace: mockMetaspace,
2059
+ space: mockSpace as any,
2060
+ });
2061
+ iReckon(sir, signedWithNewManage).isGonnaBeTruthy();
2062
+
2063
+ // Verify we can sign using the rotated 'secondary-native' pool with the new passphrase
2064
+ const syncClaim = { verb: KEYSTONE_VERB_SYNC, target: "test_target_sync" };
2065
+ const signedWithNewSync = await service.sign({
2066
+ latestKeystone: rotated,
2067
+ masterSecret: newPassphrase,
2068
+ claim: syncClaim,
2069
+ poolId: "secondary-native",
2070
+ metaspace: mockMetaspace,
2071
+ space: mockSpace as any,
2072
+ });
2073
+ iReckon(sir, signedWithNewSync).isGonnaBeTruthy();
2074
+
2075
+ // Verify signing with the OLD master secret fails on both pools
2076
+ let oldManageFailed = false;
2077
+ try {
2078
+ await service.sign({
2079
+ latestKeystone: rotated,
2080
+ masterSecret: userSecret,
2081
+ claim: manageClaim,
2082
+ poolId: POOL_ID_MANAGE,
2083
+ metaspace: mockMetaspace,
2084
+ space: mockSpace as any,
2085
+ });
2086
+ } catch {
2087
+ oldManageFailed = true;
2088
+ }
2089
+ iReckon(sir, oldManageFailed).isGonnaBeTrue();
2090
+
2091
+ let oldSyncFailed = false;
2092
+ try {
2093
+ await service.sign({
2094
+ latestKeystone: rotated,
2095
+ masterSecret: userSecret,
2096
+ claim: syncClaim,
2097
+ poolId: "secondary-native",
2098
+ metaspace: mockMetaspace,
2099
+ space: mockSpace as any,
2100
+ });
2101
+ } catch {
2102
+ oldSyncFailed = true;
2103
+ }
2104
+ iReckon(sir, oldSyncFailed).isGonnaBeTrue();
2105
+ } catch (error) {
2106
+ console.error(`${lc} ${extractErrorMsg(error)}`);
2107
+ throw error;
2108
+ } finally {
2109
+ if (logalot) { console.log(`${lc} complete.`); }
1819
2110
  }
1820
- const custodianPool: KeystoneChallengePool = {
1821
- id: POOL_ID_CUSTODIAN_MANAGE,
1822
- config: custodianManageConfig,
1823
- challenges,
1824
- isForeign: true
1825
- };
2111
+ });
1826
2112
 
1827
- const keystone = await service.genesis({
1828
- masterSecret: userSecret,
1829
- configs: [userManageConfig],
1830
- metaspace: mockMetaspace,
1831
- space: mockSpace as any,
1832
- });
2113
+ await ifWeMight(sir, 'fails validation if password rotation details or constraints are violated', async () => {
2114
+ const lc = `[fails validation if password rotation details or constraints are violated]`;
2115
+ try {
2116
+ if (logalot) { console.log(`${lc} starting... (I: b497f8486c58b25922d28b5efbd24826)`); }
2117
+
2118
+ // Construct invalid change-password transition
2119
+ const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: "salt1" });
2120
+ userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
2121
+
2122
+ const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: "salt2" });
2123
+ custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
2124
+
2125
+ // Custodian Pool genesis
2126
+ const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
2127
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
2128
+ const challenges: { [id: string]: any } = {};
2129
+ for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
2130
+ const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
2131
+ const challengeId = raw.substring(0, 16);
2132
+ const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
2133
+ challenges[challengeId] = await strategy.generateChallenge({ solution });
2134
+ }
2135
+ const custodianPool: KeystoneChallengePool = {
2136
+ id: POOL_ID_CUSTODIAN_MANAGE,
2137
+ config: custodianManageConfig,
2138
+ challenges,
2139
+ isForeign: true
2140
+ };
1833
2141
 
1834
- const keystoneWithCustodian = await service.addPools({
1835
- latestKeystone: keystone,
1836
- masterSecret: userSecret,
1837
- newPools: [custodianPool],
1838
- metaspace: mockMetaspace,
1839
- space: mockSpace as any,
1840
- });
2142
+ const keystone = await service.genesis({
2143
+ masterSecret: userSecret,
2144
+ configs: [userManageConfig],
2145
+ metaspace: mockMetaspace,
2146
+ space: mockSpace as any,
2147
+ });
1841
2148
 
1842
- // Rotate custodian credentials to new secret
1843
- const newCustodianSecret = "NewCustodianSecret_888";
1844
- const newCustodianConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: "new_custodian_salt" });
1845
- newCustodianConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
2149
+ const keystoneWithCustodian = await service.addPools({
2150
+ latestKeystone: keystone,
2151
+ masterSecret: userSecret,
2152
+ newPools: [custodianPool],
2153
+ metaspace: mockMetaspace,
2154
+ space: mockSpace as any,
2155
+ });
1846
2156
 
1847
- const newStrategy = KeystoneStrategyFactory.create({ config: newCustodianConfig });
1848
- const newPoolSecret = await newStrategy.derivePoolSecret({ masterSecret: newCustodianSecret });
1849
- const newChallenges: { [id: string]: any } = {};
1850
- for (let i = 0; i < newCustodianConfig.behavior.size; i++) {
1851
- const raw = await hash({ s: `${newCustodianConfig.salt}${Date.now()}${i}` });
1852
- const challengeId = raw.substring(0, 16);
1853
- const solution = await newStrategy.generateSolution({ poolSecret: newPoolSecret, poolId: newCustodianConfig.id, challengeId });
1854
- newChallenges[challengeId] = await newStrategy.generateChallenge({ solution });
2157
+ // Create an invalid frame manually where a foreign pool is rotated during change-password
2158
+ const badRotatedPool = await rotatePoolChallenges({
2159
+ prevPool: custodianPool,
2160
+ newMasterSecret: "bad_secret"
2161
+ });
2162
+
2163
+ const { newKeystone: evolved } = await service.changePassword({
2164
+ keystoneIbGib: keystoneWithCustodian,
2165
+ newMasterSecret: "new_pass",
2166
+ signingSecret: userSecret,
2167
+ signingPoolId: POOL_ID_MANAGE,
2168
+ metaspace: mockMetaspace,
2169
+ space: mockSpace as any,
2170
+ });
2171
+
2172
+ // Inject the foreign pool rotation manually to simulate an attacker
2173
+ const attackerData = {
2174
+ ...evolved.data!,
2175
+ challengePools: evolved.data!.challengePools.map(p => p.id === POOL_ID_CUSTODIAN_MANAGE ? badRotatedPool : p)
2176
+ };
2177
+ // Update details claim to say we rotated custodian-manage
2178
+ attackerData.proofs[0].claim.details = {
2179
+ type: KeystoneClaimType.change_password,
2180
+ info: { change: [POOL_ID_MANAGE, POOL_ID_CUSTODIAN_MANAGE] } as ClaimDetails_ChangePassword
2181
+ };
2182
+
2183
+ const attackerKeystone = {
2184
+ ...evolved,
2185
+ data: attackerData
2186
+ };
2187
+
2188
+ const errors = await service.validate({
2189
+ prevIbGib: keystoneWithCustodian,
2190
+ currentIbGib: attackerKeystone
2191
+ });
2192
+
2193
+ iReckon(sir, errors.length > 0).isGonnaBeTrue();
2194
+ const hasForeignViolation = errors.some(e => e.includes("Security Violation") && e.includes("foreign pool"));
2195
+ iReckon(sir, hasForeignViolation).isGonnaBeTrue();
2196
+ } catch (error) {
2197
+ console.error(`${lc} ${extractErrorMsg(error)}`);
2198
+ throw error;
2199
+ } finally {
2200
+ if (logalot) { console.log(`${lc} complete.`); }
1855
2201
  }
1856
- const newCustodianPool: KeystoneChallengePool = {
1857
- id: POOL_ID_CUSTODIAN_MANAGE,
1858
- config: newCustodianConfig,
1859
- challenges: newChallenges,
1860
- isForeign: true
1861
- };
2202
+ });
1862
2203
 
1863
- // Rotate signed by user manage pool
1864
- const rotated = await service.replacePool({
1865
- latestKeystone: keystoneWithCustodian,
1866
- newPool: newCustodianPool,
1867
- signingSecret: userSecret,
1868
- signingPoolId: POOL_ID_MANAGE,
1869
- metaspace: mockMetaspace,
1870
- space: mockSpace as any,
1871
- });
2204
+ await ifWeMight(sir, 'verifies signing secrets correctly for a pool', async () => {
2205
+ const lc = `[verifies signing secrets correctly for a pool]`;
2206
+ try {
2207
+ if (logalot) { console.log(`${lc} starting... (I: 343eb84f5768ff0adcf2308c68404826)`); }
2208
+
2209
+ const userSecret = "alice_secret_123";
2210
+ const builder = KeystoneProfileBuilder.buildKeystone('low')
2211
+ .withUsername('alice')
2212
+ .withEmail('alice@example.com')
2213
+ .withDescription('Alice test keystone');
2214
+
2215
+ const configs = await builder.compileConfigs();
2216
+ const keystone = await service.genesis({
2217
+ masterSecret: userSecret,
2218
+ configs,
2219
+ frameDetails: builder.getFrameDetails(),
2220
+ metaspace: mockMetaspace,
2221
+ space: mockSpace as any,
2222
+ });
2223
+
2224
+ // 1. Correct secret on manage pool
2225
+ const isCorrectManage = await service.verifySigningSecret({
2226
+ keystoneIbGib: keystone,
2227
+ signingSecret: userSecret,
2228
+ poolId: POOL_ID_MANAGE
2229
+ });
2230
+ iReckon(sir, isCorrectManage).asTo('correct manage secret').isGonnaBeTrue();
2231
+
2232
+ // 2. Incorrect secret on manage pool
2233
+ const isWrongManage = await service.verifySigningSecret({
2234
+ keystoneIbGib: keystone,
2235
+ signingSecret: "wrong_secret",
2236
+ poolId: POOL_ID_MANAGE
2237
+ });
2238
+ iReckon(sir, isWrongManage).asTo('incorrect manage secret').isGonnaBeFalse();
2239
+
2240
+ // 3. Correct secret on default/sign pool
2241
+ const isCorrectDefault = await service.verifySigningSecret({
2242
+ keystoneIbGib: keystone,
2243
+ signingSecret: userSecret,
2244
+ poolId: 'default'
2245
+ });
2246
+ iReckon(sir, isCorrectDefault).asTo('correct default secret').isGonnaBeTrue();
2247
+
2248
+ // 4. Invalid/non-existent pool ID
2249
+ const isInvalidPool = await service.verifySigningSecret({
2250
+ keystoneIbGib: keystone,
2251
+ signingSecret: userSecret,
2252
+ poolId: 'invalid_pool_id_123'
2253
+ });
2254
+ iReckon(sir, isInvalidPool).asTo('invalid pool id').isGonnaBeFalse();
2255
+ } catch (error) {
2256
+ console.error(`${lc} ${extractErrorMsg(error)}`);
2257
+ throw error;
2258
+ } finally {
2259
+ if (logalot) { console.log(`${lc} complete.`); }
2260
+ }
2261
+ });
2262
+
2263
+ await ifWeMight(sir, 'removes pools successfully, validating transitions and auth', async () => {
2264
+ const lc = `[removes pools successfully, validating transitions and auth]`;
2265
+ try {
2266
+ if (logalot) { console.log(`${lc} starting... (I: 2e2e747d6808b05c1e452a680d94b726)`); }
2267
+
2268
+ const userSecret = "alice_secret_123";
2269
+ const customPoolId = "custodian-manage-google";
2270
+ const configs = [
2271
+ createStandardPoolConfig({
2272
+ id: POOL_ID_MANAGE,
2273
+ salt: "manage_salt",
2274
+ }),
2275
+ createStandardPoolConfig({
2276
+ id: customPoolId,
2277
+ salt: "custodian_salt",
2278
+ })
2279
+ ];
2280
+
2281
+ const keystone = await service.genesis({
2282
+ masterSecret: userSecret,
2283
+ configs,
2284
+ metaspace: mockMetaspace,
2285
+ space: mockSpace as any,
2286
+ });
2287
+
2288
+ // 1. Success path: remove customPoolId using correct secret
2289
+ const evolvedKeystone = await service.removePools({
2290
+ latestKeystone: keystone,
2291
+ masterSecret: userSecret,
2292
+ poolIds: [customPoolId],
2293
+ metaspace: mockMetaspace,
2294
+ space: mockSpace as any,
2295
+ });
2296
+
2297
+ iReckon(sir, evolvedKeystone).asTo('evolved keystone exists').isGonnaBeTruthy();
2298
+ iReckon(sir, evolvedKeystone.data?.challengePools.length).asTo('pool count reduced').willEqual(1);
2299
+ iReckon(sir, evolvedKeystone.data?.challengePools[0].id).asTo('manage pool remains').willEqual(POOL_ID_MANAGE);
2300
+
2301
+ // Verify transition is fully valid
2302
+ const errors = await service.validate({
2303
+ prevIbGib: keystone,
2304
+ currentIbGib: evolvedKeystone
2305
+ });
2306
+ iReckon(sir, errors.length).asTo('transition validation succeeds').willEqual(0);
2307
+
2308
+ // 2. Sad path: invalid master secret
2309
+ let cryptoErrorCaught = false;
2310
+ try {
2311
+ await service.removePools({
2312
+ latestKeystone: keystone,
2313
+ masterSecret: "wrong_secret",
2314
+ poolIds: [customPoolId],
2315
+ metaspace: mockMetaspace,
2316
+ space: mockSpace as any,
2317
+ });
2318
+ } catch (e: any) {
2319
+ if (e.message.includes('Crypto Violation')) {
2320
+ cryptoErrorCaught = true;
2321
+ }
2322
+ }
2323
+ iReckon(sir, cryptoErrorCaught).asTo('forgery check prevents removal').isGonnaBeTrue();
2324
+
2325
+ // 3. Sad path: non-existent pool ID
2326
+ let notFoundErrorCaught = false;
2327
+ try {
2328
+ await service.removePools({
2329
+ latestKeystone: keystone,
2330
+ masterSecret: userSecret,
2331
+ poolIds: ["non-existent-pool"],
2332
+ metaspace: mockMetaspace,
2333
+ space: mockSpace as any,
2334
+ });
2335
+ } catch (e: any) {
2336
+ if (e.message.includes('not found in keystone')) {
2337
+ notFoundErrorCaught = true;
2338
+ }
2339
+ }
2340
+ iReckon(sir, notFoundErrorCaught).asTo('non-existent pool ID rejected').isGonnaBeTrue();
2341
+ } catch (error) {
2342
+ console.error(`${lc} ${extractErrorMsg(error)}`);
2343
+ throw error;
2344
+ } finally {
2345
+ if (logalot) { console.log(`${lc} complete.`); }
2346
+ }
2347
+ });
2348
+
2349
+ await ifWeMight(sir, 'rotates registered delegate keystone challenge pools when changing user password', async () => {
2350
+ const lc = `[rotates registered delegate keystone challenge pools when changing user password]`;
2351
+ try {
2352
+ if (logalot) { console.log(`${lc} starting...`); }
2353
+
2354
+ const userSecret = "alice_initial_pass_123";
2355
+ const newSecret = "alice_new_pass_456";
2356
+
2357
+ // 1. Create primary keystone
2358
+ const parentConfigs = [
2359
+ createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: "parent_manage_salt" }),
2360
+ createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: "parent_default_salt" })
2361
+ ];
2362
+ const parentKeystone = await service.genesis({
2363
+ masterSecret: userSecret,
2364
+ configs: parentConfigs,
2365
+ metaspace: mockMetaspace,
2366
+ space: mockSpace as any,
2367
+ });
2368
+
2369
+ // 2. Create delegate keystone using derived delegateSecret from userSecret (matching real app creation behavior)
2370
+ const delegateSecret = await deriveDelegateSecret({ masterSecret: userSecret });
2371
+ const delegateConfigs = [
2372
+ createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: "delegate_manage_salt" }),
2373
+ createStandardPoolConfig({ id: "delegate_sync_pool", salt: "delegate_sync_salt" })
2374
+ ];
2375
+ const delegateKeystone = await service.genesis({
2376
+ masterSecret: delegateSecret,
2377
+ configs: delegateConfigs,
2378
+ metaspace: mockMetaspace,
2379
+ space: mockSpace as any,
2380
+ });
2381
+
2382
+ const delegateTjp = getTjpAddr({ ibGib: delegateKeystone });
2383
+ if (!delegateTjp) { throw new Error('delegateTjp missing'); }
2384
+
2385
+ // 3. Register delegate under primary keystone
2386
+ const registeredParent = await service.registerDelegate({
2387
+ parentKeystone,
2388
+ delegateKeystone,
2389
+ masterSecret: userSecret,
2390
+ metaspace: mockMetaspace,
2391
+ space: mockSpace as any,
2392
+ allowedVerbs: [KEYSTONE_VERB_SYNC],
2393
+ });
2394
+
2395
+ // 4. Change user password on parent keystone.
2396
+ // Note: We explicitly set `poolIds: undefined` to reinforce that passing undefined
2397
+ // instructs changePassword to rotate all non-foreign pools on the primary keystone
2398
+ // AND all registered outward delegates. (In production caller code, poolIds parameter can simply be omitted).
2399
+ const { newKeystone: evolvedParent, newDelegates } = await service.changePassword({
2400
+ keystoneIbGib: registeredParent,
2401
+ newMasterSecret: newSecret,
2402
+ signingSecret: userSecret,
2403
+ signingPoolId: POOL_ID_MANAGE,
2404
+ poolIds: undefined,
2405
+ metaspace: mockMetaspace,
2406
+ space: mockSpace as any,
2407
+ });
2408
+
2409
+ // 5. Retrieve latest delegate keystone from metaspace/space
2410
+ if (newDelegates && newDelegates.length === 1) {
2411
+ const latestDelegate = newDelegates[0];
2412
+
2413
+ // 6. Verify primary keystone challenge pools have rotated to new secret
2414
+ for (const pool of evolvedParent.data!.challengePools) {
2415
+ if (!pool.isForeign) {
2416
+ const oldValid = await service.verifySigningSecret({
2417
+ keystoneIbGib: evolvedParent,
2418
+ signingSecret: userSecret,
2419
+ poolId: pool.id
2420
+ });
2421
+ iReckon(sir, oldValid).asTo(`parent pool ${pool.id} old secret invalid`).isGonnaBeFalse();
2422
+
2423
+ const newValid = await service.verifySigningSecret({
2424
+ keystoneIbGib: evolvedParent,
2425
+ signingSecret: newSecret,
2426
+ poolId: pool.id
2427
+ });
2428
+ iReckon(sir, newValid).asTo(`parent pool ${pool.id} new secret valid`).isGonnaBeTrue();
2429
+ }
2430
+ }
2431
+
2432
+ // 7. Verify delegate keystone challenge pools have rotated to new secret
2433
+ const oldDelegateSecret = await deriveDelegateSecret({ masterSecret: userSecret });
2434
+ const newDelegateSecret = await deriveDelegateSecret({ masterSecret: newSecret });
2435
+ for (const pool of latestDelegate.data!.challengePools) {
2436
+ if (!pool.isForeign) {
2437
+ const oldValid = await service.verifySigningSecret({
2438
+ keystoneIbGib: latestDelegate,
2439
+ signingSecret: oldDelegateSecret,
2440
+ poolId: pool.id
2441
+ });
2442
+ iReckon(sir, oldValid).asTo(`delegate pool ${pool.id} old secret invalid`).isGonnaBeFalse();
2443
+
2444
+ const newValid = await service.verifySigningSecret({
2445
+ keystoneIbGib: latestDelegate,
2446
+ signingSecret: newDelegateSecret,
2447
+ poolId: pool.id
2448
+ });
2449
+ iReckon(sir, newValid).asTo(`delegate pool ${pool.id} new secret valid`).isGonnaBeTrue();
2450
+ }
2451
+ }
2452
+
2453
+ } else {
2454
+ iReckon(sir, true).asTo('newDelegates should have had length 1 but is empty/falsy').isGonnaBeFalse();
2455
+ }
2456
+
2457
+ } catch (error) {
2458
+ console.error(`${lc} ${extractErrorMsg(error)}`);
2459
+ throw error;
2460
+ } finally {
2461
+ if (logalot) { console.log(`${lc} complete.`); }
2462
+ }
2463
+ });
2464
+ });
1872
2465
 
1873
- iReckon(sir, rotated).isGonnaBeTruthy();
1874
-
1875
- // Verify we can sign with new custodian secret
1876
- const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
1877
- const signedNew = await service.sign({
1878
- latestKeystone: rotated,
1879
- masterSecret: newCustodianSecret,
1880
- claim,
1881
- poolId: POOL_ID_CUSTODIAN_MANAGE,
1882
- metaspace: mockMetaspace,
1883
- space: mockSpace as any,
1884
- });
1885
- iReckon(sir, signedNew).isGonnaBeTruthy();
1886
- });
1887
-
1888
- await ifWeMight(sir, 'rotates all non-foreign pools during changePassword by default', async () => {
1889
- const userSalt = "user_salt";
1890
- const custodianSalt = "custodian_salt";
1891
- const secondarySalt = "secondary_salt";
1892
-
1893
- const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
1894
- userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1895
-
1896
- // Another native/non-foreign pool
1897
- const secondaryNativeConfig = createStandardPoolConfig({ id: "secondary-native", salt: secondarySalt });
1898
- secondaryNativeConfig.allowedVerbs = [KEYSTONE_VERB_SYNC];
1899
-
1900
- // A custodian/foreign pool
1901
- const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
1902
- custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
1903
-
1904
- // Custodian Pool genesis
1905
- const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
1906
- const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
1907
- const challenges: { [id: string]: any } = {};
1908
- for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
1909
- const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
1910
- const challengeId = raw.substring(0, 16);
1911
- const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
1912
- challenges[challengeId] = await strategy.generateChallenge({ solution });
1913
- }
1914
- const custodianPool: KeystoneChallengePool = {
1915
- id: POOL_ID_CUSTODIAN_MANAGE,
1916
- config: custodianManageConfig,
1917
- challenges,
1918
- isForeign: true
1919
- };
1920
-
1921
- const keystone = await service.genesis({
1922
- masterSecret: userSecret,
1923
- configs: [userManageConfig, secondaryNativeConfig],
1924
- metaspace: mockMetaspace,
1925
- space: mockSpace as any,
1926
- });
1927
-
1928
- const keystoneWithCustodian = await service.addPools({
1929
- latestKeystone: keystone,
1930
- masterSecret: userSecret,
1931
- newPools: [custodianPool],
1932
- metaspace: mockMetaspace,
1933
- space: mockSpace as any,
1934
- });
1935
-
1936
- // Rotate passphrase/password
1937
- const newPassphrase = "SuperNewSecretPassphrase_999";
1938
- const rotated = await service.changePassword({
1939
- latestKeystone: keystoneWithCustodian,
1940
- newMasterSecret: newPassphrase,
1941
- signingSecret: userSecret,
1942
- signingPoolId: POOL_ID_MANAGE,
1943
- metaspace: mockMetaspace,
1944
- space: mockSpace as any,
1945
- });
1946
-
1947
- iReckon(sir, rotated).isGonnaBeTruthy();
1948
-
1949
- const data = rotated.data!;
1950
- const rotatedManage = data.challengePools.find(p => p.id === POOL_ID_MANAGE)!;
1951
- const rotatedSecondary = data.challengePools.find(p => p.id === "secondary-native")!;
1952
- const rotatedCustodian = data.challengePools.find(p => p.id === POOL_ID_CUSTODIAN_MANAGE)!;
1953
-
1954
- // Verify native pools rotated (salt changed)
1955
- iReckon(sir, rotatedManage.config.salt).not.willEqual(userSalt);
1956
- iReckon(sir, rotatedSecondary.config.salt).not.willEqual(secondarySalt);
1957
-
1958
- // Verify foreign pool was not modified (salt identical)
1959
- iReckon(sir, rotatedCustodian.config.salt).willEqual(custodianSalt);
1960
-
1961
- // Verify we can validate transition
1962
- const errors = await service.validate({
1963
- prevIbGib: keystoneWithCustodian,
1964
- currentIbGib: rotated
1965
- });
1966
- iReckon(sir, errors.length).willEqual(0);
1967
-
1968
- // Verify we can sign using the rotated 'manage' pool with the new passphrase
1969
- const manageClaim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target_manage" };
1970
- const signedWithNewManage = await service.sign({
1971
- latestKeystone: rotated,
1972
- masterSecret: newPassphrase,
1973
- claim: manageClaim,
1974
- poolId: POOL_ID_MANAGE,
1975
- metaspace: mockMetaspace,
1976
- space: mockSpace as any,
1977
- });
1978
- iReckon(sir, signedWithNewManage).isGonnaBeTruthy();
1979
-
1980
- // Verify we can sign using the rotated 'secondary-native' pool with the new passphrase
1981
- const syncClaim = { verb: KEYSTONE_VERB_SYNC, target: "test_target_sync" };
1982
- const signedWithNewSync = await service.sign({
1983
- latestKeystone: rotated,
1984
- masterSecret: newPassphrase,
1985
- claim: syncClaim,
1986
- poolId: "secondary-native",
1987
- metaspace: mockMetaspace,
1988
- space: mockSpace as any,
1989
- });
1990
- iReckon(sir, signedWithNewSync).isGonnaBeTruthy();
1991
-
1992
- // Verify signing with the OLD master secret fails on both pools
1993
- let oldManageFailed = false;
1994
- try {
1995
- await service.sign({
1996
- latestKeystone: rotated,
1997
- masterSecret: userSecret,
1998
- claim: manageClaim,
1999
- poolId: POOL_ID_MANAGE,
2000
- metaspace: mockMetaspace,
2001
- space: mockSpace as any,
2002
- });
2003
- } catch {
2004
- oldManageFailed = true;
2005
- }
2006
- iReckon(sir, oldManageFailed).isGonnaBeTrue();
2007
-
2008
- let oldSyncFailed = false;
2009
- try {
2010
- await service.sign({
2011
- latestKeystone: rotated,
2012
- masterSecret: userSecret,
2013
- claim: syncClaim,
2014
- poolId: "secondary-native",
2015
- metaspace: mockMetaspace,
2016
- space: mockSpace as any,
2017
- });
2018
- } catch {
2019
- oldSyncFailed = true;
2020
- }
2021
- iReckon(sir, oldSyncFailed).isGonnaBeTrue();
2022
- });
2023
-
2024
- await ifWeMight(sir, 'fails validation if password rotation details or constraints are violated', async () => {
2025
- // Construct invalid change-password transition
2026
- const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: "salt1" });
2027
- userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
2028
-
2029
- const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: "salt2" });
2030
- custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
2031
-
2032
- // Custodian Pool genesis
2033
- const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
2034
- const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
2035
- const challenges: { [id: string]: any } = {};
2036
- for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
2037
- const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
2038
- const challengeId = raw.substring(0, 16);
2039
- const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
2040
- challenges[challengeId] = await strategy.generateChallenge({ solution });
2041
- }
2042
- const custodianPool: KeystoneChallengePool = {
2043
- id: POOL_ID_CUSTODIAN_MANAGE,
2044
- config: custodianManageConfig,
2045
- challenges,
2046
- isForeign: true
2047
- };
2048
-
2049
- const keystone = await service.genesis({
2050
- masterSecret: userSecret,
2051
- configs: [userManageConfig],
2052
- metaspace: mockMetaspace,
2053
- space: mockSpace as any,
2054
- });
2055
-
2056
- const keystoneWithCustodian = await service.addPools({
2057
- latestKeystone: keystone,
2058
- masterSecret: userSecret,
2059
- newPools: [custodianPool],
2060
- metaspace: mockMetaspace,
2061
- space: mockSpace as any,
2062
- });
2063
-
2064
- // Create an invalid frame manually where a foreign pool is rotated during change-password
2065
- const badRotatedPool = await rotatePoolChallenges({
2066
- prevPool: custodianPool,
2067
- newMasterSecret: "bad_secret"
2068
- });
2069
-
2070
- const evolved = await service.changePassword({
2071
- latestKeystone: keystoneWithCustodian,
2072
- newMasterSecret: "new_pass",
2073
- signingSecret: userSecret,
2074
- signingPoolId: POOL_ID_MANAGE,
2075
- metaspace: mockMetaspace,
2076
- space: mockSpace as any,
2077
- });
2078
-
2079
- // Inject the foreign pool rotation manually to simulate an attacker
2080
- const attackerData = {
2081
- ...evolved.data!,
2082
- challengePools: evolved.data!.challengePools.map(p => p.id === POOL_ID_CUSTODIAN_MANAGE ? badRotatedPool : p)
2083
- };
2084
- // Update details claim to say we rotated custodian-manage
2085
- attackerData.proofs[0].claim.details = {
2086
- type: KeystoneClaimType.change_password,
2087
- info: { change: [POOL_ID_MANAGE, POOL_ID_CUSTODIAN_MANAGE] } as ClaimDetails_ChangePassword
2088
- };
2089
-
2090
- const attackerKeystone = {
2091
- ...evolved,
2092
- data: attackerData
2093
- };
2094
-
2095
- const errors = await service.validate({
2096
- prevIbGib: keystoneWithCustodian,
2097
- currentIbGib: attackerKeystone
2098
- });
2099
-
2100
- iReckon(sir, errors.length > 0).isGonnaBeTrue();
2101
- const hasForeignViolation = errors.some(e => e.includes("Security Violation") && e.includes("foreign pool"));
2102
- iReckon(sir, hasForeignViolation).isGonnaBeTrue();
2103
- });
2104
-
2105
- await ifWeMight(sir, 'verifies signing secrets correctly for a pool', async () => {
2106
- const userSecret = "alice_secret_123";
2107
- const builder = KeystoneProfileBuilder.buildKeystone('low')
2108
- .withUsername('alice')
2109
- .withEmail('alice@example.com')
2110
- .withDescription('Alice test keystone');
2111
-
2112
- const configs = await builder.compileConfigs();
2113
- const keystone = await service.genesis({
2114
- masterSecret: userSecret,
2115
- configs,
2116
- frameDetails: builder.getFrameDetails(),
2117
- metaspace: mockMetaspace,
2118
- space: mockSpace as any,
2119
- });
2120
-
2121
- // 1. Correct secret on manage pool
2122
- const isCorrectManage = await service.verifySigningSecret({
2123
- keystoneIbGib: keystone,
2124
- signingSecret: userSecret,
2125
- poolId: POOL_ID_MANAGE
2126
- });
2127
- iReckon(sir, isCorrectManage).asTo('correct manage secret').isGonnaBeTrue();
2128
-
2129
- // 2. Incorrect secret on manage pool
2130
- const isWrongManage = await service.verifySigningSecret({
2131
- keystoneIbGib: keystone,
2132
- signingSecret: "wrong_secret",
2133
- poolId: POOL_ID_MANAGE
2134
- });
2135
- iReckon(sir, isWrongManage).asTo('incorrect manage secret').isGonnaBeFalse();
2136
-
2137
- // 3. Correct secret on default/sign pool
2138
- const isCorrectDefault = await service.verifySigningSecret({
2139
- keystoneIbGib: keystone,
2140
- signingSecret: userSecret,
2141
- poolId: 'default'
2142
- });
2143
- iReckon(sir, isCorrectDefault).asTo('correct default secret').isGonnaBeTrue();
2144
-
2145
- // 4. Invalid/non-existent pool ID
2146
- const isInvalidPool = await service.verifySigningSecret({
2147
- keystoneIbGib: keystone,
2148
- signingSecret: userSecret,
2149
- poolId: 'invalid_pool_id_123'
2150
- });
2151
- iReckon(sir, isInvalidPool).asTo('invalid pool id').isGonnaBeFalse();
2152
- });
2153
-
2154
- await ifWeMight(sir, 'removes pools successfully, validating transitions and auth', async () => {
2155
- const userSecret = "alice_secret_123";
2156
- const customPoolId = "custodian-manage-google";
2157
- const configs = [
2158
- createStandardPoolConfig({
2159
- id: POOL_ID_MANAGE,
2160
- salt: "manage_salt",
2161
- }),
2162
- createStandardPoolConfig({
2163
- id: customPoolId,
2164
- salt: "custodian_salt",
2165
- })
2166
- ];
2167
-
2168
- const keystone = await service.genesis({
2169
- masterSecret: userSecret,
2170
- configs,
2171
- metaspace: mockMetaspace,
2172
- space: mockSpace as any,
2173
- });
2174
-
2175
- // 1. Success path: remove customPoolId using correct secret
2176
- const evolvedKeystone = await service.removePools({
2177
- latestKeystone: keystone,
2178
- masterSecret: userSecret,
2179
- poolIds: [customPoolId],
2180
- metaspace: mockMetaspace,
2181
- space: mockSpace as any,
2182
- });
2183
-
2184
- iReckon(sir, evolvedKeystone).asTo('evolved keystone exists').isGonnaBeTruthy();
2185
- iReckon(sir, evolvedKeystone.data?.challengePools.length).asTo('pool count reduced').willEqual(1);
2186
- iReckon(sir, evolvedKeystone.data?.challengePools[0].id).asTo('manage pool remains').willEqual(POOL_ID_MANAGE);
2187
-
2188
- // Verify transition is fully valid
2189
- const errors = await service.validate({
2190
- prevIbGib: keystone,
2191
- currentIbGib: evolvedKeystone
2192
- });
2193
- iReckon(sir, errors.length).asTo('transition validation succeeds').willEqual(0);
2194
-
2195
- // 2. Sad path: invalid master secret
2196
- let cryptoErrorCaught = false;
2197
- try {
2198
- await service.removePools({
2199
- latestKeystone: keystone,
2200
- masterSecret: "wrong_secret",
2201
- poolIds: [customPoolId],
2202
- metaspace: mockMetaspace,
2203
- space: mockSpace as any,
2204
- });
2205
- } catch (e: any) {
2206
- if (e.message.includes('Crypto Violation')) {
2207
- cryptoErrorCaught = true;
2208
- }
2209
- }
2210
- iReckon(sir, cryptoErrorCaught).asTo('forgery check prevents removal').isGonnaBeTrue();
2211
-
2212
- // 3. Sad path: non-existent pool ID
2213
- let notFoundErrorCaught = false;
2214
- try {
2215
- await service.removePools({
2216
- latestKeystone: keystone,
2217
- masterSecret: userSecret,
2218
- poolIds: ["non-existent-pool"],
2219
- metaspace: mockMetaspace,
2220
- space: mockSpace as any,
2221
- });
2222
- } catch (e: any) {
2223
- if (e.message.includes('not found in keystone')) {
2224
- notFoundErrorCaught = true;
2225
- }
2226
- }
2227
- iReckon(sir, notFoundErrorCaught).asTo('non-existent pool ID rejected').isGonnaBeTrue();
2228
- });
2229
- });
2230
-