@@ -323,8 +323,9 @@ export function setCustomStringifier(method: (input: unknown) => string, name: s
323323 * Returns a signature obtained by signing the input hash (hex string or buffer) with the sk string
324324 * @param input
325325 * @param sk
326+ * @param detached - If true (default), returns only the signature (64 bytes). If false, returns signature + message
326327 */
327- export function sign ( input : hexstring | Buffer , sk : secretKey | Buffer ) : string {
328+ export function sign ( input : hexstring | Buffer , sk : secretKey | Buffer , detached = true ) : string {
328329 let inputBuf : Buffer
329330 let skBuf : Buffer
330331 if ( typeof input !== 'string' ) {
@@ -353,9 +354,65 @@ export function sign(input: hexstring | Buffer, sk: secretKey | Buffer): string
353354 throw new TypeError ( 'Secret key string must be in hex format' )
354355 }
355356 }
356- const sig = Buffer . allocUnsafe ( inputBuf . length + sodium . crypto_sign_BYTES )
357+ if ( detached ) {
358+ // Use detached signature
359+ const sig = Buffer . allocUnsafe ( sodium . crypto_sign_BYTES )
360+ try {
361+ sodium . crypto_sign_detached ( sig , inputBuf , skBuf )
362+ } catch ( e ) {
363+ throw new Error ( 'Failed to sign input with provided secret key.' )
364+ }
365+ return sig . toString ( 'hex' )
366+ } else {
367+ // Use non-detached signature (legacy behavior)
368+ const sig = Buffer . allocUnsafe ( inputBuf . length + sodium . crypto_sign_BYTES )
369+ try {
370+ sodium . crypto_sign ( sig , inputBuf , skBuf )
371+ } catch ( e ) {
372+ throw new Error ( 'Failed to sign input with provided secret key.' )
373+ }
374+ return sig . toString ( 'hex' )
375+ }
376+ }
377+
378+ /**
379+ * Returns a detached signature obtained by signing the input hash (hex string or buffer) with the sk string
380+ * @param input - The message to sign (hex string or buffer)
381+ * @param sk - The secret key
382+ * @returns Only the 64-byte signature as hex string
383+ */
384+ export function signDetached ( input : hexstring | Buffer , sk : secretKey | Buffer ) : string {
385+ let inputBuf : Buffer
386+ let skBuf : Buffer
387+ if ( typeof input !== 'string' ) {
388+ if ( Buffer . isBuffer ( input ) ) {
389+ inputBuf = input
390+ } else {
391+ throw new TypeError ( 'Input must be a hex string or buffer.' )
392+ }
393+ } else {
394+ try {
395+ inputBuf = Buffer . from ( input , 'hex' )
396+ } catch ( e ) {
397+ throw new TypeError ( 'Input string must be in hex format.' )
398+ }
399+ }
400+ if ( typeof sk !== 'string' ) {
401+ if ( Buffer . isBuffer ( sk ) ) {
402+ skBuf = sk
403+ } else {
404+ throw new TypeError ( 'Secret key must be a hex string or buffer.' )
405+ }
406+ } else {
407+ try {
408+ skBuf = Buffer . from ( sk , 'hex' )
409+ } catch ( e ) {
410+ throw new TypeError ( 'Secret key string must be in hex format' )
411+ }
412+ }
413+ const sig = Buffer . allocUnsafe ( sodium . crypto_sign_BYTES )
357414 try {
358- sodium . crypto_sign ( sig , inputBuf , skBuf )
415+ sodium . crypto_sign_detached ( sig , inputBuf , skBuf )
359416 } catch ( e ) {
360417 throw new Error ( 'Failed to sign input with provided secret key.' )
361418 }
@@ -368,9 +425,33 @@ export function sign(input: hexstring | Buffer, sk: secretKey | Buffer): string
368425 * @param obj
369426 * @param sk
370427 * @param pk
428+ * @param detached - If true (default), uses detached signature (64 bytes only). If false, uses non-detached
429+ * @returns the new signed object with the `sign` field. The original object is mutated as well.
430+ */
431+ export function signObj ( obj : object , sk : secretKey | Buffer , pk : publicKey | Buffer , detached = true ) : SignedObject {
432+ if ( typeof obj !== 'object' ) {
433+ throw new TypeError ( 'Input must be an object.' )
434+ }
435+ // If it's an array, we don't want to try to sign it
436+ if ( Array . isArray ( obj ) ) {
437+ throw new TypeError ( 'Input cannot be an array.' )
438+ }
439+ const objStr = stringify ( obj )
440+ const hashed = hash ( objStr , 'buffer' )
441+ const sig = sign ( hashed , sk , detached )
442+ const signPk = Buffer . isBuffer ( pk ) ? bufferToHex ( pk ) : pk
443+ ; ( obj as SignedObject ) . sign = { owner : signPk , sig }
444+ return obj as SignedObject
445+ }
446+
447+ /**
448+ * Attaches a sign field to the input object using detached signatures
449+ * @param obj - The object to sign
450+ * @param sk - The secret key
451+ * @param pk - The public key
371452 * @returns the new signed object with the `sign` field. The original object is mutated as well.
372453 */
373- export function signObj ( obj : object , sk : secretKey | Buffer , pk : publicKey | Buffer ) : SignedObject {
454+ export function signObjDetached ( obj : object , sk : secretKey | Buffer , pk : publicKey | Buffer ) : SignedObject {
374455 if ( typeof obj !== 'object' ) {
375456 throw new TypeError ( 'Input must be an object.' )
376457 }
@@ -380,7 +461,7 @@ export function signObj(obj: object, sk: secretKey | Buffer, pk: publicKey | Buf
380461 }
381462 const objStr = stringify ( obj )
382463 const hashed = hash ( objStr , 'buffer' )
383- const sig = sign ( hashed , sk )
464+ const sig = signDetached ( hashed , sk )
384465 const signPk = Buffer . isBuffer ( pk ) ? bufferToHex ( pk ) : pk
385466 ; ( obj as SignedObject ) . sign = { owner : signPk , sig }
386467 return obj as SignedObject
@@ -402,13 +483,59 @@ function verify(msg: string, sig: hexstring | Buffer, pk: publicKey | Buffer): b
402483 }
403484 const sigBuf = _ensureBuffer ( sig )
404485 const pkBuf = _ensureBuffer ( pk )
486+
487+ // Auto-detect signature type based on length
488+ // Detached signatures are exactly 64 bytes
489+ // Non-detached signatures are 64 bytes + message length
490+ const msgBuf = Buffer . from ( msg , 'hex' )
491+ const expectedNonDetachedLength = sodium . crypto_sign_BYTES + msgBuf . length
492+
493+ if ( sigBuf . length === sodium . crypto_sign_BYTES ) {
494+ // This is a detached signature
495+ try {
496+ return sodium . crypto_sign_verify_detached ( sigBuf as Buffer , msgBuf , pkBuf as Buffer )
497+ } catch ( e ) {
498+ return false
499+ }
500+ } else if ( sigBuf . length === expectedNonDetachedLength ) {
501+ // This is a non-detached signature
502+ try {
503+ const opened = Buffer . allocUnsafe ( sigBuf . length - sodium . crypto_sign_BYTES )
504+ sodium . crypto_sign_open ( opened , sigBuf as Buffer , pkBuf as Buffer )
505+ const verified = opened . toString ( 'hex' )
506+ return verified === msg
507+ } catch ( e ) {
508+ return false
509+ }
510+ } else {
511+ // Invalid signature length
512+ throw new Error ( 'Invalid signature length. Expected either detached (64 bytes) or non-detached signature.' )
513+ }
514+ }
515+
516+ /**
517+ * Verifies a detached signature against a message
518+ * @param msg - The message that was signed (hex string)
519+ * @param sig - The detached signature (hex string or buffer)
520+ * @param pk - The public key to verify with
521+ * @returns true if the signature is valid, false otherwise
522+ */
523+ export function verifyDetached ( msg : string , sig : hexstring | Buffer , pk : publicKey | Buffer ) : boolean {
524+ if ( typeof msg !== 'string' ) {
525+ throw new TypeError ( 'Message to compare must be a string.' )
526+ }
527+ const msgBuf = Buffer . from ( msg , 'hex' )
528+ const sigBuf = _ensureBuffer ( sig )
529+ const pkBuf = _ensureBuffer ( pk )
530+
531+ if ( sigBuf . length !== sodium . crypto_sign_BYTES ) {
532+ throw new Error ( 'Invalid signature length for detached signature.' )
533+ }
534+
405535 try {
406- const opened = Buffer . allocUnsafe ( sigBuf . length - sodium . crypto_sign_BYTES )
407- sodium . crypto_sign_open ( opened , sigBuf as Buffer , pkBuf as Buffer )
408- const verified = opened . toString ( 'hex' )
409- return verified === msg
536+ return sodium . crypto_sign_verify_detached ( sigBuf as Buffer , msgBuf , pkBuf as Buffer )
410537 } catch ( e ) {
411- throw new Error ( 'Unable to verify provided signature with provided public key.' )
538+ return false
412539 }
413540}
414541
@@ -433,6 +560,28 @@ export function verifyObj(obj: SignedObject): boolean {
433560 return verify ( objHash , obj . sign . sig , obj . sign . owner )
434561}
435562
563+ /**
564+ * Verifies an object signed with detached signature
565+ * @param obj - The signed object to verify
566+ * @returns true if the signature is valid, false otherwise
567+ */
568+ export function verifyObjDetached ( obj : SignedObject ) : boolean {
569+ if ( typeof obj !== 'object' ) {
570+ throw new TypeError ( 'Input must be an object.' )
571+ }
572+ if ( ! obj . sign || ! obj . sign . owner || ! obj . sign . sig ) {
573+ throw new Error ( 'Object must contain a sign field with the following data: { owner, sig }' )
574+ }
575+ if ( typeof obj . sign . owner !== 'string' ) {
576+ throw new TypeError ( 'Owner must be a public key represented as a hex string.' )
577+ }
578+ if ( typeof obj . sign . sig !== 'string' ) {
579+ throw new TypeError ( 'Signature must be a valid signature represented as a hex string.' )
580+ }
581+ const objHash = hashObj ( obj , true )
582+ return verifyDetached ( objHash , obj . sign . sig , obj . sign . owner )
583+ }
584+
436585/**
437586 * This function initialized the cryptographic hashing functions
438587 * @param key The HASH_KEY for initializing the cryptographic hashing functions
0 commit comments