diff --git a/nip44.test.ts b/nip44.test.ts index 088d893..7cf9d08 100644 --- a/nip44.test.ts +++ b/nip44.test.ts @@ -30,8 +30,8 @@ test('NIP44: invalid', async () => { for (const v of vectors.invalid) { expect(() => { const key = utils.v2.getConversationKey(v.sec1, v.pub2) - const ciphertext = decrypt(key, v.plaintext) - }).toThrowError() + const ciphertext = decrypt(key, v.ciphertext) + }).toThrowError(v.note) } }) @@ -39,7 +39,7 @@ test('NIP44: invalid_conversation_key', async () => { for (const v of vectors.invalid_conversation_key) { expect(() => { const key = utils.v2.getConversationKey(v.sec1, v.pub2) - const ciphertext = encrypt(key, v.plaintext) + const ciphertext = encrypt(key, 'a') }).toThrowError() } }) diff --git a/nip44.ts b/nip44.ts index 62f602b..bb4a051 100644 --- a/nip44.ts +++ b/nip44.ts @@ -39,7 +39,7 @@ export const utils = { pad(unpadded: string): Uint8Array { const unpaddedB = utf8Encoder.encode(unpadded) const len = unpaddedB.length - if (len < 1 || len >= utils.v2.maxPlaintextSize) throw new Error('plaintext should be between 1b and 64KB') + if (len < 1 || len >= utils.v2.maxPlaintextSize) throw new Error('invalid plaintext length: must be between 1b and 64KB') const paddedLen = utils.v2.calcPadding(len) const zeros = new Uint8Array(paddedLen - len) const lenBuf = new Uint8Array(2) @@ -68,38 +68,40 @@ export function encrypt( ): string { const version = options.version ?? 2 if (version !== 2) throw new Error('unknown encryption version ' + version) - const salt = options.salt ?? randomBytes(32) ensureBytes(salt, 32) - const keys = utils.v2.getMessageKeys(key, salt) const padded = utils.v2.pad(plaintext) const ciphertext = chacha20(keys.encryption, keys.nonce, padded) const mac = hmac(sha256, keys.auth, ciphertext) - return base64.encode(concatBytes(new Uint8Array([version]), salt, ciphertext, mac)) } export function decrypt(key: Uint8Array, ciphertext: string): string { + const u = utils.v2 + ensureBytes(key, 32) + const clen = ciphertext.length + if (clen < u.minCiphertextSize || clen >= u.maxCiphertextSize) throw new Error('invalid ciphertext length: ' + clen) - if (clen < utils.v2.minCiphertextSize || clen >= utils.v2.maxCiphertextSize) - throw new Error('ciphertext length is invalid') - - const v = ciphertext[0] - if (v === '#') throw new Error('unknown encryption version') - const data = base64.decode(ciphertext) - const version = data.subarray(0, 1)[0] - if (version !== 2) throw new Error('unknown encryption version ' + version) + if (ciphertext[0] === '#') throw new Error('unknown encryption version') + let data: Uint8Array + try { + data = base64.decode(ciphertext) + } catch (error) { + throw new Error('invalid base64: ' + (error as any).message) + } + const vers = data.subarray(0, 1)[0] + if (vers !== 2) throw new Error('unknown encryption version ' + vers) const salt = data.subarray(1, 33) const ciphertext_ = data.subarray(33, -32) const mac = data.subarray(-32) - const keys = utils.v2.getMessageKeys(key, salt) + const keys = u.getMessageKeys(key, salt) const calculatedMac = hmac(sha256, keys.auth, ciphertext_) - if (!equalBytes(calculatedMac, mac)) throw new Error('encryption MAC does not match') + if (!equalBytes(calculatedMac, mac)) throw new Error('invalid MAC') - const plaintext = chacha20(keys.encryption, keys.nonce, ciphertext_) - return utils.v2.unpad(plaintext) + const padded = chacha20(keys.encryption, keys.nonce, ciphertext_) + return u.unpad(padded) } diff --git a/nip44.vectors.json b/nip44.vectors.json index c3b60b8..4737427 100644 --- a/nip44.vectors.json +++ b/nip44.vectors.json @@ -26,7 +26,7 @@ "salt": "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b", "plaintext": "表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀", "ciphertext": "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7yuU7XwJ8wCYUrq4aXX86HLnkMx7fPFvNeMk0uek9ma01magfEBIf+vJvZdWKiv48eUu9Cv31plAJsH6kSIsGc5TVYBYipkrQUNRxxJA15QT+uCURF96v3XuSS0k2Pf108AI=", - "note": "hard-unicode string" + "note": "unicode-heavy string" }, { "sec1": "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c", @@ -82,8 +82,7 @@ "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", "salt": "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54", "plaintext": "🙈 🙉 🙊 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗", - "ciphertext": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtU9bUC2ji2A2udRI2VCEQZ7IAmYRRgxodBtd5Yi/5htCUczf1jLHxIt9AhVAZLKuRgbWOuEMq5RBybkxPsSeAkxzXVOlWHZ1Febq5ogkjqY/6Xj8CwwmaZxfbx+d1BKKO3Wa+IFuXwuVAZa1Xo+fan+skyf+2R5QSj10QGAnGO7odAu/iZ9A28eMoSNeXsdxqy1+PRt5Zk4i019xmf7C4PDGSzgFZSvQ2EzusJN5WcsnRFmF1L5rXpX1AYo8HusOpWcGf9PjmFbO+8spUkX1W/T21GRm4o7dro1Y6ycgGOA9BsiQ==", - "note": "emoji and lang 7" + "ciphertext": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtU9bUC2ji2A2udRI2VCEQZ7IAmYRRgxodBtd5Yi/5htCUczf1jLHxIt9AhVAZLKuRgbWOuEMq5RBybkxPsSeAkxzXVOlWHZ1Febq5ogkjqY/6Xj8CwwmaZxfbx+d1BKKO3Wa+IFuXwuVAZa1Xo+fan+skyf+2R5QSj10QGAnGO7odAu/iZ9A28eMoSNeXsdxqy1+PRt5Zk4i019xmf7C4PDGSzgFZSvQ2EzusJN5WcsnRFmF1L5rXpX1AYo8HusOpWcGf9PjmFbO+8spUkX1W/T21GRm4o7dro1Y6ycgGOA9BsiQ==" } ], "valid_pub": [ @@ -121,9 +120,9 @@ "pub2": "8348c2d35549098706e5bab7966d9a9c72fbf6554e918f41c2b6cb275f79ec13", "sharedKey": "8673ec68393a997bfad7eab8661461daf8b3931b7e885d78312a3fb7fe17f41a", "salt": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", - "plaintext": "n o s t r", - "ciphertext": "##Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbOsrsqIEybscEwg5rnI/Cx03mDSmeweOLKD7dw5BDZQDxXSlCwX1LIcTJEZaJPTz98Ftu0zSE0d93ED7OtdlvNeZx", - "note": "invalid version: flag" + "plaintext": "n o b l e", + "ciphertext": "##Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbOsrsqIEyf8ccwhlrnI/Cx03mDSmeweOLKD7dw5BDZQDxXe2FwUJ8Ag25VoJ4MGhjlPCNmCU/Uqk4k0jwbhgR3fRh", + "note": "unknown encryption version" }, { "sec1": "11063318c5cb3cd9cafcced42b4db5ea02ec976ed995962d2bc1fa1e9b52e29f", @@ -131,8 +130,8 @@ "sharedKey": "e2aad10de00913088e5cb0f73fa526a6a17e95763cc5b2a127022f5ea5a73445", "salt": "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781", "plaintext": "⚠️", - "ciphertext": "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvPSc+7YCIFTmGk5OLuh1nhl6TvID7sGKLFUCWRW1eRfV/0a7sT46N3nTQzD7IE67zLWrYqGnE+0DDNz6sJ4hAaFrT", - "note": "invalid version: 0" + "ciphertext": "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBA4fZmoHrtrz5I5pCHuwWZ22qqL/Xt1VidEZGMLds0yaJ5VwUbeEifEJlPICOFt1ssZJxCUf43HvRwCVTFskbhSMh", + "note": "unknown encryption version 0" }, { "sec1": "2573d1e9b9ac5de5d570f652cbb9e8d4f235e3d3d334181448e87c417f374e83", @@ -149,8 +148,8 @@ "sharedKey": "2e70c0a1cde884b88392458ca86148d859b273a5695ede5bbe41f731d7d88ffd", "salt": "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25", "plaintext": "¯\\_(ツ)_/¯", - "ciphertext": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholdUejFZPARM22IvOqp1U/UmFSkeSyTBYbbwy5ykmi+mKiER/Pr3IhMJbShCKkP4ytxzWxEndwVjRV+ZgzmeGNL7Dy", - "note": "invalid mac 1" + "ciphertext": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholdUejFZPARM22IvOqp1U/UmFSkeSyTBYbbwy5ykmi+mKiEcWL+nVmTOf28MMiC+rTpZys/8p1hqQFpn+XWZRPrVay", + "note": "invalid MAC" }, { "sec1": "067eda13c4a36090ad28a7a183e9df611186ca01f63cb30fcdfa615ebfd6fb6d", @@ -158,8 +157,8 @@ "sharedKey": "a808915e31afc5b853d654d2519632dac7298ee2ecddc11695b8eba925935c2a", "salt": "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4", "plaintext": "🥎", - "ciphertext": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0U0P/M1/oKm95z8qz6Kg0zQawLzwk3DskvWA2drXP4zK+t0BULZ9vhTDlmL8rsvBozBsvQwFPqd63PRRS4zrFvgwh", - "note": "invalid mac 2" + "ciphertext": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0U0P/M1/oKm95z8qz6Kg0zQawLzwk3DskvWA2drXP4zK+tzHpKvWq0KOdx5MdypboSQsP4NXfhh2KoUffjkyIOiMA", + "note": "invalid MAC" }, { "sec1": "3e7be560fb9f8c965c48953dbd00411d48577e200cf00d7cc427e49d0e8d9c01", @@ -168,7 +167,7 @@ "salt": "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1", "plaintext": "elliptic-curve cryptography", "ciphertext": "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHBu7F9m36yBd58mVUBB5ktBTOJREDaQT1KAyPmZidP+IRea1lNw5YAEK7+pbnpfCw8CD0i2n8Pf2IDWlKDhLiVvatw", - "note": "padding invalid: u16 length is 0" + "note": "invalid padding" }, { "sec1": "c22e1d4de967aa39dc143354d8f596cec1d7c912c3140831fff2976ce3e387c1", @@ -177,7 +176,7 @@ "salt": "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330", "plaintext": "Peer-to-Peer", "ciphertext": "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwhT0hvSnF9Xjp9Lml792qtNbmAVvR6laukTe9eYEjeWPpZFxtkVpYTbbL9wDKFeplDMKsUKVa+roSeSvv0ela9seDVl2Sfso=", - "note": "invalid padding: 5 extra zeros" + "note": "invalid padding" }, { "sec1": "be1edab14c5912e5c59084f197f0945242e969c363096cccb59af8898815096f", @@ -186,38 +185,33 @@ "salt": "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b", "plaintext": "censorship-resistant and global social network", "ciphertext": "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6bL9HUMo3Mjkz4bjQo/FJF2LWHmaCr9Byc3hU9D7we+EkNBWenBHasT1G52fZk9r3NKeOC1hLezNwBLr7XXiULh+NbMBDtJh9/aQh1uZ9EpAfeISOzbZXwYwf0P5M85g9XER8hZ2fgJDLb4qMOuQRG6CrPezhr357nS3UHwPC2qHo3uKACxhE+2td+965yDcvMTx4KYTQg1zNhd7PA5v/WPnWeq2B623yLxlevUuo/OvXplFho3QVy7s5QZVop6qV2g2/l/SIsvD0HIcv3V35sywOCBR0K4VHgduFqkx/LEF3NGgAbjONXQHX8ZKushsEeR4TxlFoRSovAyYjhWolz+Ok3KJL2Ertds3H+M/Bdl2WnZGT0IbjZjn3DS+b1Ke0R0X4Onww2ZG3+7o6ncIwTc+lh1O7YQn00V0HJ+EIp03heKV2zWdVSC615By/+Yt9KAiV56n5+02GAuNqA", - "note": "invalid padding: 250 extra zeros" + "note": "invalid padding" } ], "invalid_conversation_key": [ { "sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "plaintext": "a", "note": "sec1 higher than curve.n" }, { "sec1": "0000000000000000000000000000000000000000000000000000000000000000", "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "plaintext": "a", "note": "sec1 is 0" }, { "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", "pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "plaintext": "a", "note": "pub2 is invalid, no sqrt, all-ff" }, { "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "plaintext": "a", "note": "sec1 == curve.n" }, { "sec1": "0000000000000000000000000000000000000000000000000000000000000002", "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "plaintext": "a", "note": "pub2 is invalid, no sqrt" } ],