mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(desktop): reject empty encrypted token payloads
This commit is contained in:
parent
6cb459af9e
commit
3553c1b313
2 changed files with 44 additions and 1 deletions
|
|
@ -326,3 +326,34 @@ test('an unusable keychain fails the write loudly and writes nothing', () => {
|
|||
// ...and must not clobber the tokens already on disk.
|
||||
assert.equal(broken.fileText(), before)
|
||||
})
|
||||
|
||||
test('an encrypt that returns null is refused rather than blanking the stored entry', () => {
|
||||
const existing = createFakeDisk()
|
||||
|
||||
persistNativeTokenSet(GATEWAY, TOKENS, existing.io)
|
||||
|
||||
const before = existing.fileText()
|
||||
const nulled = createFakeDisk(before, { encrypt: () => null })
|
||||
let writes = 0
|
||||
|
||||
// Spy that still delegates, so a stray write would show up in BOTH the
|
||||
// counter and the file text.
|
||||
const io = {
|
||||
...nulled.io,
|
||||
writeStoreText: (text: string) => {
|
||||
writes += 1
|
||||
nulled.io.writeStoreText(text)
|
||||
}
|
||||
}
|
||||
|
||||
// A quiet null is the same failure as a throw and must be just as loud.
|
||||
assert.throws(
|
||||
() => persistNativeTokenSet(GATEWAY, { ...TOKENS, accessToken: 'AT-new' }, io),
|
||||
/refusing to overwrite stored native tokens/
|
||||
)
|
||||
assert.equal(writes, 0, 'the store file must not be written at all')
|
||||
// Byte-for-byte unchanged...
|
||||
assert.equal(nulled.fileText(), before)
|
||||
// ...and the original token set still loads, refresh token intact.
|
||||
assert.deepEqual(loadNativeTokenSet(GATEWAY, createFakeDisk(before).io), TOKENS)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export interface NativeTokenStoreIo {
|
|||
/**
|
||||
* Encrypt one plaintext blob. main.ts passes the strict safeStorage helper,
|
||||
* which THROWS when the OS keychain is unavailable — that must stay loud.
|
||||
* A `null` return is treated as the same authoritative failure: the caller
|
||||
* throws rather than persisting an empty entry over good tokens.
|
||||
*/
|
||||
encrypt: (plaintext: string) => StoredTokenSecret | null
|
||||
/** Decrypt a stored payload; returns '' when it cannot be read. */
|
||||
|
|
@ -76,7 +78,17 @@ export function persistNativeTokenSet(baseUrl: string, tokens: NativeTokenSet |
|
|||
// plaintext on disk. Deliberately outside the try below: an unusable
|
||||
// keychain is an authoritative write failure and must surface to the
|
||||
// caller, not be logged away as if the tokens were saved.
|
||||
store[baseUrl] = io.encrypt(JSON.stringify(tokens))
|
||||
const secret = io.encrypt(JSON.stringify(tokens))
|
||||
|
||||
if (!secret) {
|
||||
// A null blob is the same failure as a throw, only quieter. Storing it
|
||||
// would replace a good entry with nothing: the write would report
|
||||
// success, the next launch would show signed out, and the refresh token
|
||||
// would be unrecoverable. Fail before touching the store.
|
||||
throw new Error('Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.')
|
||||
}
|
||||
|
||||
store[baseUrl] = secret
|
||||
} else {
|
||||
delete store[baseUrl]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue