Skip to content

Token generation: KV tokens#60

Draft
corrideat wants to merge 1 commit intomainfrom
ephemeral-messages
Draft

Token generation: KV tokens#60
corrideat wants to merge 1 commit intomainfrom
ephemeral-messages

Conversation

@corrideat
Copy link
Member

@corrideat corrideat commented Mar 9, 2026

This is a preliminary implementation of KV programmatic tokens, part of the ephemeral messages series.

This is missing more robust checks, a full implementation for files and testing. In addition, this PR doesn't cover changes required in Group Income to make use of these APIs.


Open with Devin

@corrideat corrideat self-assigned this Mar 9, 2026
@corrideat corrideat marked this pull request as draft March 9, 2026 03:39
@socket-security
Copy link

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​chelonia/​crypto@​1.0.1 ⏵ 1.0.2100 +27100100 +1100 +22100 +31

View full report

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

deletionTokenHint = token!.hint
deletionTokenHash = blake32Hash(deletionToken)
} else {
deletionToken = deletionTokenFromHint(state, uploader.keyName, contractID, key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 deletionTokenFromHint called with wrong argument order — uploader.hint is never passed

The call at src/chelonia.ts:2422 passes (state, uploader.keyName, contractID, key), but the function signature at src/utils.ts:1283-1284 is (state, signingKeyName, hint, objectCid, kvKey?). This means contractID is passed as the hint parameter (should be uploader.hint), key (the KV key) is passed as objectCid (should be contractID), and the actual KV key is never passed as kvKey. The uploader.hint value, which is the whole reason this branch exists (uploader.hint != null), is completely ignored, so the wrong key will be looked up and the wrong deletion token will be generated.

Suggested change
deletionToken = deletionTokenFromHint(state, uploader.keyName, contractID, key)
deletionToken = deletionTokenFromHint(state, uploader.keyName, uploader.hint, contractID, key)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +53 to +56
.sort((a, b) => {
if (a._notAfterHeight == null) return 1
if (b._notAfterHeight == null) return 1
return a._notAfterHeight - b._notAfterHeight
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 findAllKeyIdsByName sort comparator violates antisymmetry, producing non-deterministic ordering

The sort comparator at src/utils.ts:53-56 returns 1 both when a._notAfterHeight == null (line 54) and when b._notAfterHeight == null (line 55). This violates the antisymmetry requirement of comparison functions: when a is revoked and b is active, it returns 1 (a after b); but when a is active and b is revoked, it also returns 1 (a after b). These are contradictory orderings for the same pair, making Array.sort() non-deterministic. Since this function is used in freshDeletionToken (line 1275) to compute a stable index-based hint, and in deletionTokenFromHint (line 1286) to reconstruct the key from that hint, non-deterministic ordering means the hint may point to the wrong key, producing an invalid deletion token.

Suggested change
.sort((a, b) => {
if (a._notAfterHeight == null) return 1
if (b._notAfterHeight == null) return 1
return a._notAfterHeight - b._notAfterHeight
.sort((a, b) => {
if (a._notAfterHeight == null && b._notAfterHeight == null) return 0
if (a._notAfterHeight == null) return 1
if (b._notAfterHeight == null) return -1
return a._notAfterHeight - b._notAfterHeight
})
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1273 to +1274
const key = (sbp('chelonia/rootState') as ChelRootState).secretKeys[signingKeyId]
const token = sign(key, tokenData).slice(0, 24)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 sign called with serialized key string instead of deserialized Key object

In both freshDeletionToken (src/utils.ts:1273-1274) and deletionTokenFromHint (src/utils.ts:1291-1292), the key is read directly from rootState.secretKeys[signingKeyId] which stores serialized strings (typed as Record<string, string> at src/types.ts:349). The sign function is then called with this raw string. Every other call site in the codebase deserializes the key first: buildShelterAuthorizationHeader at src/utils.ts:1080 does deserializeKey(SAK), and signedData.ts:126 uses deserializedKey. Missing deserializeKey() will cause sign to receive a string instead of the expected Key object, likely causing a runtime error or incorrect signature.

Prompt for agents
In src/utils.ts, both freshDeletionToken (line 1273-1274) and deletionTokenFromHint (line 1291-1292) need to deserialize the key before calling sign(). Import deserializeKey from @chelonia/crypto (it's already imported at line 2). Then change:

Line 1273-1274 in freshDeletionToken:
  const key = (sbp('chelonia/rootState') as ChelRootState).secretKeys[signingKeyId]
  const token = sign(key, tokenData).slice(0, 24)
to:
  const serializedKey = (sbp('chelonia/rootState') as ChelRootState).secretKeys[signingKeyId]
  const token = sign(deserializeKey(serializedKey), tokenData).slice(0, 24)

Line 1291-1292 in deletionTokenFromHint:
  const key = (sbp('chelonia/rootState') as ChelRootState).secretKeys[signingKeyId]
  const token = sign(key, tokenData).slice(0, 24)
to:
  const serializedKey = (sbp('chelonia/rootState') as ChelRootState).secretKeys[signingKeyId]
  const token = sign(deserializeKey(serializedKey), tokenData).slice(0, 24)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant