• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KeychainMDIP / kc / 14936596541

09 May 2025 07:50PM UTC coverage: 95.132% (-0.04%) from 95.176%
14936596541

Pull #816

github

macterra
Update node version for unit test workflow
Pull Request #816: chore: Upgrade node to v22.15.0 LTS

1151 of 1268 branches covered (90.77%)

Branch coverage included in aggregate %.

18 of 18 new or added lines in 3 files covered. (100.0%)

2 existing lines in 2 files now uncovered.

2386 of 2450 relevant lines covered (97.39%)

471.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.55
/packages/cipher/src/cipher-node.ts
1
import * as bip39 from 'bip39';
2
import * as secp from '@noble/secp256k1';
3
import { hmac } from '@noble/hashes/hmac';
4
import { sha256 } from '@noble/hashes/sha256';
5
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
6
import { managedNonce } from '@noble/ciphers/webcrypto/utils'
7
import { bytesToUtf8, utf8ToBytes } from '@noble/ciphers/utils';
8
import { base64url } from 'multiformats/bases/base64';
9
import HDKeyNode from 'hdkey';
10
import { webcrypto } from 'node:crypto';
11
import { Cipher, HDKeyJSON, EcdsaJwkPublic, EcdsaJwkPrivate, EcdsaJwkPair } from './types.js';
12
import canonicalizeModule from 'canonicalize';
13
const canonicalize = canonicalizeModule as unknown as (input: unknown) => string;
4✔
14

15
// node.js 18 and older, requires polyfilling globalThis.crypto
16
if (!globalThis.crypto) {
4!
17
    // @ts-expect-error: globalThis may not be declared
UNCOV
18
    globalThis.crypto = webcrypto
×
19
}
20

21
// Polyfill for synchronous signatures
22
// Recommendation from https://github.com/paulmillr/noble-secp256k1/blob/main/README.md
23
secp.etc.hmacSha256Sync = (k: Uint8Array, ...m: Uint8Array[]): Uint8Array => hmac(sha256, k, secp.etc.concatBytes(...m));
11,300✔
24

25
export default class CipherNode implements Cipher {
26

27
    generateMnemonic(): string {
28
        return bip39.generateMnemonic();
544✔
29
    }
30

31
    generateHDKey(mnemonic: string): HDKeyNode {
32
        const seed = bip39.mnemonicToSeedSync(mnemonic);
556✔
33
        return HDKeyNode.fromMasterSeed(seed);
554✔
34
    }
35

36
    generateHDKeyJSON(json: HDKeyJSON): HDKeyNode {
37
        return HDKeyNode.fromJSON(json);
2,092✔
38
    }
39

40
    generateJwk(privateKeyBytes: Uint8Array): EcdsaJwkPair {
41
        const compressedPublicKeyBytes = secp.getPublicKey(privateKeyBytes);
2,952✔
42
        const compressedPublicKeyHex = secp.etc.bytesToHex(compressedPublicKeyBytes);
2,952✔
43
        const curvePoints = secp.ProjectivePoint.fromHex(compressedPublicKeyHex);
2,952✔
44
        const uncompressedPublicKeyBytes = curvePoints.toRawBytes(false); // false = uncompressed
2,952✔
45
        // we need uncompressed public key so that it contains both the x and y values for the JWK format:
46
        // the first byte is a header that indicates whether the key is uncompressed (0x04 if uncompressed).
47
        // bytes 1 - 32 represent X
48
        // bytes 33 - 64 represent Y
49
        const d = base64url.baseEncode(privateKeyBytes);
2,952✔
50
        // skip the first byte because it's used as a header to indicate whether the key is uncompressed
51
        const x = base64url.baseEncode(uncompressedPublicKeyBytes.subarray(1, 33));
2,952✔
52
        const y = base64url.baseEncode(uncompressedPublicKeyBytes.subarray(33, 65));
2,952✔
53

54
        const publicJwk: EcdsaJwkPublic = {
2,952✔
55
            // alg: 'ES256K',
56
            kty: 'EC',
57
            crv: 'secp256k1',
58
            x,
59
            y
60
        };
61

62
        const privateJwk: EcdsaJwkPrivate = { ...publicJwk, d };
2,952✔
63

64
        return { publicJwk, privateJwk };
2,952✔
65
    }
66

67
    generateRandomJwk(): EcdsaJwkPair {
68
        const privKey = secp.utils.randomPrivateKey();
250✔
69
        return this.generateJwk(privKey);
250✔
70
    }
71

72
    convertJwkToCompressedBytes(jwk: EcdsaJwkPublic): Uint8Array {
73
        const xBytes = base64url.baseDecode(jwk.x);
3,538✔
74
        const yBytes = base64url.baseDecode(jwk.y);
3,538✔
75

76
        // Determine the prefix (02 for even y, 03 for odd y)
77
        const prefix = yBytes[yBytes.length - 1] % 2 === 0 ? 0x02 : 0x03;
3,538✔
78

79
        // Construct compressed key
80
        return new Uint8Array([prefix, ...xBytes]);
3,538✔
81
    }
82

83
    hashMessage(msg: string): string {
84
        const hash = sha256(msg);
5,022✔
85
        return Buffer.from(hash).toString('hex');
5,022✔
86
    }
87

88
    hashJSON(json: unknown): string {
89
        const canonical = canonicalize(json);
4,902✔
90
        return this.hashMessage(canonical);
4,902✔
91
    }
92

93
    signHash(msgHash: string, privateJwk: EcdsaJwkPrivate): string {
94
        const privKey = base64url.baseDecode(privateJwk.d);
2,260✔
95
        const signature = secp.sign(msgHash, privKey);
2,260✔
96

97
        return signature.toCompactHex();
2,260✔
98
    }
99

100
    verifySig(msgHash: string, sigHex: string, publicJwk: EcdsaJwkPublic): boolean {
101
        const compressedPublicKeyBytes = this.convertJwkToCompressedBytes(publicJwk);
2,294✔
102
        const signature = secp.Signature.fromCompact(sigHex);
2,294✔
103

104
        return secp.verify(signature, msgHash, compressedPublicKeyBytes);
2,292✔
105
    }
106

107
    encryptMessage(pubKey: EcdsaJwkPublic, privKey: EcdsaJwkPrivate, message: string): string {
108
        const priv = base64url.baseDecode(privKey.d);
906✔
109
        const pub = this.convertJwkToCompressedBytes(pubKey);
906✔
110
        const ss = secp.getSharedSecret(priv, pub);
906✔
111
        const key = ss.slice(0, 32);
906✔
112
        const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
906✔
113
        const data = utf8ToBytes(message);
906✔
114
        const ciphertext = chacha.encrypt(data);
906✔
115

116
        return base64url.baseEncode(ciphertext);
906✔
117
    }
118

119
    decryptMessage(pubKey: EcdsaJwkPublic, privKey: EcdsaJwkPrivate, ciphertext: string): string {
120
        const priv = base64url.baseDecode(privKey.d);
338✔
121
        const pub = this.convertJwkToCompressedBytes(pubKey);
338✔
122
        const ss = secp.getSharedSecret(priv, pub);
338✔
123
        const key = ss.slice(0, 32);
338✔
124
        const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
338✔
125
        const cipherdata = base64url.baseDecode(ciphertext);
338✔
126
        const data = chacha.decrypt(cipherdata);
338✔
127

128
        return bytesToUtf8(data);
282✔
129
    }
130
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc