diff --git a/17.md b/17.md deleted file mode 100644 index e9e97ba..0000000 --- a/17.md +++ /dev/null @@ -1,211 +0,0 @@ -NIP-17 -====== - -Private Direct Messages ------------------------ - -`draft` `optional` - -This NIP defines an encrypted direct messaging scheme using [NIP-44](44.md) encryption and [NIP-59](59.md) seals and gift wraps. - -## Direct Message Kind - -Kind `14` is a chat message. `p` tags identify one or more receivers of the message. - -```jsonc -{ - "id": "", - "pubkey": "", - "created_at": "", - "kind": 14, - "tags": [ - ["p", "", ""], - ["p", "", ""], - ["e", "", ""] // if this is a reply - ["subject", ""], - // rest of tags... - ], - "content": "", -} -``` - -`.content` MUST be plain text. Fields `id` and `created_at` are required. - -An `e` tag denotes the direct parent message this post is replying to. - -`q` tags MAY be used when citing events in the `.content` with [NIP-21](21.md). - -```json -["q", " or ", "", ""] -``` - -Kind `14`s MUST never be signed. If it is signed, the message might leak to relays and become **fully public**. - -## File Message Kind - -```jsonc -{ - "id": "", - "pubkey": "", - "created_at": "", - "kind": 15, - "tags": [ - ["p", "", ""], - ["p", "", ""], - ["e", "", "", "reply"], // if this is a reply - ["subject", ""], - ["file-type", ""], - ["encryption-algorithm", ""], - ["decryption-key", ""], - ["decryption-nonce", ""], - ["x", ""], - // rest of tags... - ], - "content": "" -} -``` - -Kind `15` is used for sending encrypted file event messages: - -- `file-type`: Specifies the MIME type of the attached file (e.g., `image/jpeg`, `audio/mpeg`, etc.) before encryption. -- `encryption-algorithm`: Indicates the encryption algorithm used for encrypting the file. Supported algorithms: `aes-gcm`. -- `decryption-key`: The decryption key that will be used by the recipient to decrypt the file. -- `decryption-nonce`: The decryption nonce that will be used by the recipient to decrypt the file. -- `content`: The URL of the file (``). -- `x` containing the SHA-256 hexencoded string of the encrypted file. -- `ox` containing the SHA-256 hexencoded string of the file before encryption. -- `size` (optional) size of the encrypted file in bytes -- `dim` (optional) size in pixels in the form `x` -- `blurhash`(optional) the [blurhash](https://github.com/woltapp/blurhash) to show while the client is loading the file -- `thumb` (optional) URL of thumbnail with same aspect ratio (encrypted with the same key, nonce) -- `fallback` (optional) zero or more fallback file sources in case `url` fails (encrypted with the same key, nonce) - -Just like kind `14`, kind `15`s MUST never be signed. - -## Chat Rooms - -The set of `pubkey` + `p` tags defines a chat room. If a new `p` tag is added or a current one is removed, a new room is created with a clean message history. - -Clients SHOULD render messages of the same room in a continuous thread. - -An optional `subject` tag defines the current name/topic of the conversation. Any member can change the topic by simply submitting a new `subject` to an existing `pubkey` + `p` tags room. There is no need to send `subject` in every message. The newest `subject` in the chat room is the subject of the conversation. - -## Encrypting - -Following [NIP-59](59.md), the **unsigned** `kind:14` & `kind:15` chat messages must be sealed (`kind:13`) and then gift-wrapped (`kind:1059`) to each receiver and the sender individually. - -```js -{ - "id": "", - "pubkey": randomPublicKey, - "created_at": randomTimeUpTo2DaysInThePast(), - "kind": 1059, // gift wrap - "tags": [ - ["p", receiverPublicKey, ""] // receiver - ], - "content": nip44Encrypt( - { - "id": "", - "pubkey": senderPublicKey, - "created_at": randomTimeUpTo2DaysInThePast(), - "kind": 13, // seal - "tags": [], // no tags - "content": nip44Encrypt(unsignedKind14, senderPrivateKey, receiverPublicKey), - "sig": "" - }, - randomPrivateKey, receiverPublicKey - ), - "sig": "" -} -``` - -The encryption algorithm MUST use the latest version of [NIP-44](44.md). - -Clients MUST verify if pubkey of the `kind:13` is the same pubkey on the `kind:14`, otherwise any sender can impersonate others by simply changing the pubkey on `kind:14`. - -Clients SHOULD randomize `created_at` in up to two days in the past in both the seal and the gift wrap to make sure grouping by `created_at` doesn't reveal any metadata. - -The gift wrap's `p` tag can be the receiver's main pubkey or an alias key created to receive DMs without exposing the receiver's identity. - -Clients CAN offer disappearing messages by setting an `expiration` tag in the gift wrap of each receiver or by not generating a gift wrap to the sender's public key - -## Publishing - -Kind `10050` indicates the user's preferred relays to receive DMs. The event MUST include a list of `relay` tags with relay URIs. - -```jsonc -{ - "kind": 10050, - "tags": [ - ["relay", "wss://inbox.nostr.wine"], - ["relay", "wss://myrelay.nostr1.com"], - ], - "content": "", - // other fields... -} -``` - -Clients SHOULD publish kind `14` events to the `10050`-listed relays. If that is not found that indicates the user is not ready to receive messages under this NIP and clients shouldn't try. - -## Relays - -It's advisable that relays do not serve `kind:1059` to clients other than the ones tagged in them. - -It's advisable that users choose relays that conform to these practices. - -Clients SHOULD guide users to keep `kind:10050` lists small (1-3 relays) and SHOULD spread it to as many relays as viable. - -## Benefits & Limitations - -This NIP offers the following privacy and security features: - -1. **No Metadata Leak**: Participant identities, each message's real date and time, event kinds, and other event tags are all hidden from the public. Senders and receivers cannot be linked with public information alone. -2. **No Public Group Identifiers**: There is no public central queue, channel or otherwise converging identifier to correlate or count all messages in the same group. -3. **No Moderation**: There are no group admins: no invitations or bans. -4. **No Shared Secrets**: No secret must be known to all members that can leak or be mistakenly shared -5. **Fully Recoverable**: Messages can be fully recoverable by any client with the user's private key -6. **Optional Forward Secrecy**: Users and clients can opt-in for "disappearing messages". -7. **Uses Public Relays**: Messages can flow through public relays without loss of privacy. Private relays can increase privacy further, but they are not required. -8. **Cold Storage**: Users can unilaterally opt-in to sharing their messages with a separate key that is exclusive for DM backup and recovery. - -The main limitation of this approach is having to send a separate encrypted event to each receiver. Group chats with more than 100 participants should find a more suitable messaging scheme. - -## Implementation - -Clients implementing this NIP should by default only connect to the set of relays found in their `kind:10050` list. From that they should be able to load all messages both sent and received as well as get new live updates, making it for a very simple and lightweight implementation that should be fast. - -When sending a message to anyone, clients must then connect to the relays in the receiver's `kind:10050` and send the events there but can disconnect right after unless more messages are expected to be sent (e.g. the chat tab is still selected). Clients should also send a copy of their outgoing messages to their own `kind:10050` relay set. - -## Examples - -This example sends the message `Hola, que tal?` from `nsec1w8udu59ydjvedgs3yv5qccshcj8k05fh3l60k9x57asjrqdpa00qkmr89m` to `nsec12ywtkplvyq5t6twdqwwygavp5lm4fhuang89c943nf2z92eez43szvn4dt`. - -The two final GiftWraps, one to the receiver and the other to the sender, respectively, are: - -```json -{ - "id":"2886780f7349afc1344047524540ee716f7bdc1b64191699855662330bf235d8", - "pubkey":"8f8a7ec43b77d25799281207e1a47f7a654755055788f7482653f9c9661c6d51", - "created_at":1703128320, - "kind":1059, - "tags":[ - ["p", "918e2da906df4ccd12c8ac672d8335add131a4cf9d27ce42b3bb3625755f0788"] - ], - "content":"AsqzdlMsG304G8h08bE67dhAR1gFTzTckUUyuvndZ8LrGCvwI4pgC3d6hyAK0Wo9gtkLqSr2rT2RyHlE5wRqbCOlQ8WvJEKwqwIJwT5PO3l2RxvGCHDbd1b1o40ZgIVwwLCfOWJ86I5upXe8K5AgpxYTOM1BD+SbgI5jOMA8tgpRoitJedVSvBZsmwAxXM7o7sbOON4MXHzOqOZpALpS2zgBDXSAaYAsTdEM4qqFeik+zTk3+L6NYuftGidqVluicwSGS2viYWr5OiJ1zrj1ERhYSGLpQnPKrqDaDi7R1KrHGFGyLgkJveY/45y0rv9aVIw9IWF11u53cf2CP7akACel2WvZdl1htEwFu/v9cFXD06fNVZjfx3OssKM/uHPE9XvZttQboAvP5UoK6lv9o3d+0GM4/3zP+yO3C0NExz1ZgFmbGFz703YJzM+zpKCOXaZyzPjADXp8qBBeVc5lmJqiCL4solZpxA1865yPigPAZcc9acSUlg23J1dptFK4n3Tl5HfSHP+oZ/QS/SHWbVFCtq7ZMQSRxLgEitfglTNz9P1CnpMwmW/Y4Gm5zdkv0JrdUVrn2UO9ARdHlPsW5ARgDmzaxnJypkfoHXNfxGGXWRk0sKLbz/ipnaQP/eFJv/ibNuSfqL6E4BnN/tHJSHYEaTQ/PdrA2i9laG3vJti3kAl5Ih87ct0w/tzYfp4SRPhEF1zzue9G/16eJEMzwmhQ5Ec7jJVcVGa4RltqnuF8unUu3iSRTQ+/MNNUkK6Mk+YuaJJs6Fjw6tRHuWi57SdKKv7GGkr0zlBUU2Dyo1MwpAqzsCcCTeQSv+8qt4wLf4uhU9Br7F/L0ZY9bFgh6iLDCdB+4iABXyZwT7Ufn762195hrSHcU4Okt0Zns9EeiBOFxnmpXEslYkYBpXw70GmymQfJlFOfoEp93QKCMS2DAEVeI51dJV1e+6t3pCSsQN69Vg6jUCsm1TMxSs2VX4BRbq562+VffchvW2BB4gMjsvHVUSRl8i5/ZSDlfzSPXcSGALLHBRzy+gn0oXXJ/447VHYZJDL3Ig8+QW5oFMgnWYhuwI5QSLEyflUrfSz+Pdwn/5eyjybXKJftePBD9Q+8NQ8zulU5sqvsMeIx/bBUx0fmOXsS3vjqCXW5IjkmSUV7q54GewZqTQBlcx+90xh/LSUxXex7UwZwRnifvyCbZ+zwNTHNb12chYeNjMV7kAIr3cGQv8vlOMM8ajyaZ5KVy7HpSXQjz4PGT2/nXbL5jKt8Lx0erGXsSsazkdoYDG3U", - "sig":"a3c6ce632b145c0869423c1afaff4a6d764a9b64dedaf15f170b944ead67227518a72e455567ca1c2a0d187832cecbde7ed478395ec4c95dd3e71749ed66c480" -} -``` - -```json -{ - "id":"162b0611a1911cfcb30f8a5502792b346e535a45658b3a31ae5c178465509721", - "pubkey":"626be2af274b29ea4816ad672ee452b7cf96bbb4836815a55699ae402183f512", - "created_at":1702711587, - "kind":1059, - "tags":[ - ["p", "44900586091b284416a0c001f677f9c49f7639a55c3f1e2ec130a8e1a7998e1b"] - ], - "content":"AsTClTzr0gzXXji7uye5UB6LYrx3HDjWGdkNaBS6BAX9CpHa+Vvtt5oI2xJrmWLen+Fo2NBOFazvl285Gb3HSM82gVycrzx1HUAaQDUG6HI7XBEGqBhQMUNwNMiN2dnilBMFC3Yc8ehCJT/gkbiNKOpwd2rFibMFRMDKai2mq2lBtPJF18oszKOjA+XlOJV8JRbmcAanTbEK5nA/GnG3eGUiUzhiYBoHomj3vztYYxc0QYHOx0WxiHY8dsC6jPsXC7f6k4P+Hv5ZiyTfzvjkSJOckel1lZuE5SfeZ0nduqTlxREGeBJ8amOykgEIKdH2VZBZB+qtOMc7ez9dz4wffGwBDA7912NFS2dPBr6txHNxBUkDZKFbuD5wijvonZDvfWq43tZspO4NutSokZB99uEiRH8NAUdGTiNb25m9JcDhVfdmABqTg5fIwwTwlem5aXIy8b66lmqqz2LBzJtnJDu36bDwkILph3kmvaKPD8qJXmPQ4yGpxIbYSTCohgt2/I0TKJNmqNvSN+IVoUuC7ZOfUV9lOV8Ri0AMfSr2YsdZ9ofV5o82ClZWlWiSWZwy6ypa7CuT1PEGHzywB4CZ5ucpO60Z7hnBQxHLiAQIO/QhiBp1rmrdQZFN6PUEjFDloykoeHe345Yqy9Ke95HIKUCS9yJurD+nZjjgOxZjoFCsB1hQAwINTIS3FbYOibZnQwv8PXvcSOqVZxC9U0+WuagK7IwxzhGZY3vLRrX01oujiRrevB4xbW7Oxi/Agp7CQGlJXCgmRE8Rhm+Vj2s+wc/4VLNZRHDcwtfejogjrjdi8p6nfUyqoQRRPARzRGUnnCbh+LqhigT6gQf3sVilnydMRScEc0/YYNLWnaw9nbyBa7wFBAiGbJwO40k39wj+xT6HTSbSUgFZzopxroO3f/o4+ubx2+IL3fkev22mEN38+dFmYF3zE+hpE7jVxrJpC3EP9PLoFgFPKCuctMnjXmeHoiGs756N5r1Mm1ffZu4H19MSuALJlxQR7VXE/LzxRXDuaB2u9days/6muP6gbGX1ASxbJd/ou8+viHmSC/ioHzNjItVCPaJjDyc6bv+gs1NPCt0qZ69G+JmgHW/PsMMeL4n5bh74g0fJSHqiI9ewEmOG/8bedSREv2XXtKV39STxPweceIOh0k23s3N6+wvuSUAJE7u1LkDo14cobtZ/MCw/QhimYPd1u5HnEJvRhPxz0nVPz0QqL/YQeOkAYk7uzgeb2yPzJ6DBtnTnGDkglekhVzQBFRJdk740LEj6swkJ", - "sig":"c94e74533b482aa8eeeb54ae72a5303e0b21f62909ca43c8ef06b0357412d6f8a92f96e1a205102753777fd25321a58fba3fb384eee114bd53ce6c06a1c22bab" -} -``` diff --git a/44.md b/44.md deleted file mode 100644 index a7c13f1..0000000 --- a/44.md +++ /dev/null @@ -1,297 +0,0 @@ -NIP-44 -====== - -Encrypted Payloads (Versioned) ------------------------------- - -`optional` - -The NIP introduces a new data format for keypair-based encryption. This NIP is versioned -to allow multiple algorithm choices to exist simultaneously. This format may be used for -many things, but MUST be used in the context of a signed event as described in NIP-01. - -*Note*: this format DOES NOT define any `kind`s related to a new direct messaging standard, -only the encryption required to define one. It SHOULD NOT be used as a drop-in replacement -for NIP-04 payloads. - -## Versions - -Currently defined encryption algorithms: - -- `0x00` - Reserved -- `0x01` - Deprecated and undefined -- `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64 - -## Limitations - -Every nostr user has their own public key, which solves key distribution problems present -in other solutions. However, nostr's relay-based architecture makes it difficult to implement -more robust private messaging protocols with things like metadata hiding, forward secrecy, -and post compromise secrecy. - -The goal of this NIP is to have a _simple_ way to encrypt payloads used in the context of a signed -event. When applying this NIP to any use case, it's important to keep in mind your users' threat -model and this NIP's limitations. For high-risk situations, users should chat in specialized E2EE -messaging software and limit use of nostr to exchanging contacts. - -On its own, messages sent using this scheme have a number of important shortcomings: - -- No deniability: it is possible to prove an event was signed by a particular key -- No forward secrecy: when a key is compromised, it is possible to decrypt all previous conversations -- No post-compromise security: when a key is compromised, it is possible to decrypt all future conversations -- No post-quantum security: a powerful quantum computer would be able to decrypt the messages -- IP address leak: user IP may be seen by relays and all intermediaries between user and relay -- Date leak: `created_at` is public, since it is a part of NIP-01 event -- Limited message size leak: padding only partially obscures true message length -- No attachments: they are not supported - -Lack of forward secrecy may be partially mitigated by only sending messages to trusted relays, and asking -relays to delete stored messages after a certain duration has elapsed. - -## Version 2 - -NIP-44 version 2 has the following design characteristics: - -- Payloads are authenticated using a MAC before signing rather than afterwards because events are assumed - to be signed as specified in NIP-01. The outer signature serves to authenticate the full payload, and MUST - be validated before decrypting. -- ChaCha is used instead of AES because it's faster and has - [better security against multi-key attacks](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/). -- ChaCha is used instead of XChaCha because XChaCha has not been standardized. Also, xChaCha's improved collision - resistance of nonces isn't necessary since every message has a new (key, nonce) pair. -- HMAC-SHA256 is used instead of Poly1305 because polynomial MACs are much easier to forge. -- SHA256 is used instead of SHA3 or BLAKE because it is already used in nostr. Also BLAKE's speed advantage - is smaller in non-parallel environments. -- A custom padding scheme is used instead of padmรฉ because it provides better leakage reduction for small messages. -- Base64 encoding is used instead of another encoding algorithm because it is widely available, and is already used in nostr. - -### Encryption - -1. Calculate a conversation key - - Execute ECDH (scalar multiplication) of public key B by private key A - Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point - - Use HKDF-extract with sha256, `IKM=shared_x` and `salt=utf8_encode('nip44-v2')` - - HKDF output will be a `conversation_key` between two users. - - It is always the same, when key roles are swapped: `conv(a, B) == conv(b, A)` -2. Generate a random 32-byte nonce - - Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) - - Don't generate a nonce from message content - - Don't re-use the same nonce between messages: doing so would make them decryptable, - but won't leak the long-term key -3. Calculate message keys - - The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes long - - Use HKDF-expand, with sha256, `PRK=conversation_key`, `info=nonce` and `L=76` - - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76) -4. Add padding - - Content must be encoded from UTF-8 into byte array - - Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes - - Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]` - - Padding algorithm is related to powers-of-two, with min padded msg size of 32 bytes - - Plaintext length is encoded in big-endian as first 2 bytes of the padded blob -5. Encrypt padded content - - Use ChaCha20, with key and nonce from step 3 -6. Calculate MAC (message authentication code) - - AAD (additional authenticated data) is used - instead of calculating MAC on ciphertext, - it's calculated over a concatenation of `nonce` and `ciphertext` - - Validate that AAD (nonce) is 32 bytes -7. Base64-encode (with padding) params using `concat(version, nonce, ciphertext, mac)` - -Encrypted payloads MUST be included in an event's payload, hashed, and signed as defined in NIP 01, using schnorr -signature scheme over secp256k1. - -### Decryption - -Before decryption, the event's pubkey and signature MUST be validated as defined in NIP 01. The public key MUST be -a valid non-zero secp256k1 curve point, and the signature must be valid secp256k1 schnorr signature. For exact -validation rules, refer to BIP-340. - -1. Check if first payload's character is `#` - - `#` is an optional future-proof flag that means non-base64 encoding is used - - The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`, - implementations MUST indicate that the encryption version is not yet supported -2. Decode base64 - - Base64 is decoded into `version, nonce, ciphertext, mac` - - If the version is unknown, implementations must indicate that the encryption version is not supported - - Validate length of base64 message to prevent DoS on base64 decoder: it can be in range from 132 to 87472 chars - - Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes -3. Calculate conversation key - - See step 1 of [encryption](#Encryption) -4. Calculate message keys - - See step 3 of [encryption](#Encryption) -5. Calculate MAC (message authentication code) with AAD and compare - - Stop and throw an error if MAC doesn't match the decoded one from step 2 - - Use constant-time comparison algorithm -6. Decrypt ciphertext - - Use ChaCha20 with key and nonce from step 3 -7. Remove padding - - Read the first two BE bytes of plaintext that correspond to plaintext length - - Verify that the length of sliced plaintext matches the value of the two BE bytes - - Verify that calculated padding from step 3 of the [encryption](#Encryption) process matches the actual padding - -### Details - -- Cryptographic methods - - `secure_random_bytes(length)` fetches randomness from CSPRNG. - - `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869) - with SHA256 hash function comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)`. - - `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439) with - starting counter set to 0. - - `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104). - - `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by scalar a (`a โ‹… B`), defined in - [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki). - The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method - `bytes(P)` from BIP340. Private and public keys must be validated as per BIP340: pubkey must be a valid, - on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]`. - NIP44 doesn't do hashing of the output: keep this in mind, because some libraries hash it using sha256. - As an example, in libsecp256k1, unhashed version is available in `secp256k1_ec_pubkey_tweak_mul` -- Operators - - `x[i:j]`, where `x` is a byte array and `i, j <= 0` returns a `(j - i)`-byte array with a copy of the - `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`. -- Constants `c`: - - `min_plaintext_size` is 1. 1 byte msg is padded to 32 bytes. - - `max_plaintext_size` is 65535 (64kB - 1). It is padded to 65536 bytes. -- Functions - - `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding) - - `concat` refers to byte array concatenation - - `is_equal_ct(a, b)` is constant-time equality check of 2 byte arrays - - `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back - - `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array - - `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array - - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros - - `floor(number)` and `log2(number)` are well-known mathematical methods - -### Implementation pseudocode - -The following is a collection of python-like pseudocode functions which implement the above primitives, -intended to guide implementers. A collection of implementations in different languages is available at https://github.com/paulmillr/nip44. - -```py -# Calculates length of the padded byte array. -def calc_padded_len(unpadded_len): - next_power = 1 << (floor(log2(unpadded_len - 1))) + 1 - if next_power <= 256: - chunk = 32 - else: - chunk = next_power / 8 - if unpadded_len <= 32: - return 32 - else: - return chunk * (floor((len - 1) / chunk) + 1) - -# Converts unpadded plaintext to padded bytearray -def pad(plaintext): - unpadded = utf8_encode(plaintext) - unpadded_len = len(plaintext) - if (unpadded_len < c.min_plaintext_size or - unpadded_len > c.max_plaintext_size): raise Exception('invalid plaintext length') - prefix = write_u16_be(unpadded_len) - suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len) - return concat(prefix, unpadded, suffix) - -# Converts padded bytearray to unpadded plaintext -def unpad(padded): - unpadded_len = read_uint16_be(padded[0:2]) - unpadded = padded[2:2+unpadded_len] - if (unpadded_len == 0 or - len(unpadded) != unpadded_len or - len(padded) != 2 + calc_padded_len(unpadded_len)): raise Exception('invalid padding') - return utf8_decode(unpadded) - -# metadata: always 65b (version: 1b, nonce: 32b, max: 32b) -# plaintext: 1b to 0xffff -# padded plaintext: 32b to 0xffff -# ciphertext: 32b+2 to 0xffff+2 -# raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) -# compressed payload (base64): 132b to 87472b -def decode_payload(payload): - plen = len(payload) - if plen == 0 or payload[0] == '#': raise Exception('unknown version') - if plen < 132 or plen > 87472: raise Exception('invalid payload size') - data = base64_decode(payload) - dlen = len(d) - if dlen < 99 or dlen > 65603: raise Exception('invalid data size'); - vers = data[0] - if vers != 2: raise Exception('unknown version ' + vers) - nonce = data[1:33] - ciphertext = data[33:dlen - 32] - mac = data[dlen - 32:dlen] - return (nonce, ciphertext, mac) - -def hmac_aad(key, message, aad): - if len(aad) != 32: raise Exception('AAD associated data must be 32 bytes'); - return hmac(sha256, key, concat(aad, message)); - -# Calculates long-term key between users A and B: `get_key(Apriv, Bpub) == get_key(Bpriv, Apub)` -def get_conversation_key(private_key_a, public_key_b): - shared_x = secp256k1_ecdh(private_key_a, public_key_b) - return hkdf_extract(IKM=shared_x, salt=utf8_encode('nip44-v2')) - -# Calculates unique per-message key -def get_message_keys(conversation_key, nonce): - if len(conversation_key) != 32: raise Exception('invalid conversation_key length') - if len(nonce) != 32: raise Exception('invalid nonce length') - keys = hkdf_expand(OKM=conversation_key, info=nonce, L=76) - chacha_key = keys[0:32] - chacha_nonce = keys[32:44] - hmac_key = keys[44:76] - return (chacha_key, chacha_nonce, hmac_key) - -def encrypt(plaintext, conversation_key, nonce): - (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce) - padded = pad(plaintext) - ciphertext = chacha20(key=chacha_key, nonce=chacha_nonce, data=padded) - mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce) - return base64_encode(concat(write_u8(2), nonce, ciphertext, mac)) - -def decrypt(payload, conversation_key): - (nonce, ciphertext, mac) = decode_payload(payload) - (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce) - calculated_mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce) - if not is_equal_ct(calculated_mac, mac): raise Exception('invalid MAC') - padded_plaintext = chacha20(key=chacha_key, nonce=chacha_nonce, data=ciphertext) - return unpad(padded_plaintext) - -# Usage: -# conversation_key = get_conversation_key(sender_privkey, recipient_pubkey) -# nonce = secure_random_bytes(32) -# payload = encrypt('hello world', conversation_key, nonce) -# 'hello world' == decrypt(payload, conversation_key) -``` - -### Audit - -The v2 of the standard was audited by [Cure53](https://cure53.de) in December 2023. -Check out [audit-2023.12.pdf](https://github.com/paulmillr/nip44/blob/ce63c2eaf345e9f7f93b48f829e6bdeb7e7d7964/audit-2023.12.pdf) -and [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf). - -### Tests and code - -A collection of implementations in different languages is available at https://github.com/paulmillr/nip44. - -We publish extensive test vectors. Instead of having it in the document directly, a sha256 checksum of vectors is provided: - - 269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json - -Example of a test vector from the file: - -```json -{ - "sec1": "0000000000000000000000000000000000000000000000000000000000000001", - "sec2": "0000000000000000000000000000000000000000000000000000000000000002", - "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", - "nonce": "0000000000000000000000000000000000000000000000000000000000000001", - "plaintext": "a", - "payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb" -} -``` - -The file also contains intermediate values. A quick guidance with regards to its usage: - -- `valid.get_conversation_key`: calculate conversation_key from secret key sec1 and public key pub2 -- `valid.get_message_keys`: calculate chacha_key, chacha_nonce, hmac_key from conversation_key and nonce -- `valid.calc_padded_len`: take unpadded length (first value), calculate padded length (second value) -- `valid.encrypt_decrypt`: emulate real conversation. Calculate pub2 from sec2, verify conversation_key from (sec1, pub2), encrypt, verify payload, then calculate pub1 from sec1, verify conversation_key from (sec2, pub1), decrypt, verify plaintext. -- `valid.encrypt_decrypt_long_msg`: same as previous step, but instead of a full plaintext and payload, their checksum is provided. -- `invalid.encrypt_msg_lengths` -- `invalid.get_conversation_key`: calculating conversation_key must throw an error -- `invalid.decrypt`: decrypting message content must throw an error diff --git a/README.md b/README.md index daba767..491adec 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,27 @@ Configure persistent floating tab for login/logout: ```javascript await NOSTR_LOGIN_LITE.init({ + // Set the initial theme (default: 'default') + theme: 'dark', // Choose from 'default' or 'dark' + + // Standard configuration options + methods: { + extension: true, + local: true, + readonly: true, + connect: true, + otp: true + }, + + // Floating tab configuration (now uses theme-aware text icons) floatingTab: { enabled: true, hPosition: 0.95, // 0.0-1.0 or '95%' from left vPosition: 0.5, // 0.0-1.0 or '50%' from top appearance: { style: 'pill', // 'pill', 'square', 'circle', 'minimal' - theme: 'auto', // 'auto', 'light', 'dark' - icon: '๐Ÿ”', + theme: 'auto', // 'auto' follows main theme + icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET] text: 'Login' }, behavior: { @@ -27,6 +40,14 @@ await NOSTR_LOGIN_LITE.init({ } } }); + +// After initialization, you can switch themes dynamically: +NOSTR_LOGIN_LITE.switchTheme('dark'); +NOSTR_LOGIN_LITE.switchTheme('default'); + +// Or customize individual theme variables: +NOSTR_LOGIN_LITE.setThemeVariable('--nl-accent-color', '#00ff00'); + ``` Control methods: diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index a9fed21..0000000 --- a/examples/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# NOSTR_LOGIN_LITE Examples - -This directory contains examples and tests for NOSTR_LOGIN_LITE using the local bundle setup. - -## Files - -### ๐Ÿ”ฌ comprehensive-test.html -**The main diagnostic and testing tool** -- Comprehensive test suite with extensive debugging output -- Tests all library functions, dependencies, crypto, storage, etc. -- Results displayed on webpage for easy copying and debugging -- Run this when you need to diagnose issues or verify functionality - -### ๐Ÿ“ฑ simple-demo.html -Basic demonstration of NOSTR_LOGIN_LITE integration -- Minimal setup example -- Good starting point for new implementations - -### ๐ŸŽจ modal-login-demo.html -Demonstrates modal-based login flow -- Shows how to trigger and handle the login modal -- Example of auth event handling - -### ๐Ÿ‘ค login-and-profile.html -Login and user profile demonstration -- Shows authentication flow -- Displays user profile information after login - -### ๐Ÿ”— nip46-bunker-demo.html -NIP-46 remote signing demonstration -- Shows how to connect to remote signers/bunkers -- Advanced use case example - -## Usage - -1. Start a local web server (e.g., `python -m http.server 5501` or Live Server in VS Code) -2. Navigate to any HTML file -3. For comprehensive testing and debugging, use `comprehensive-test.html` - -All examples use the local bundle setup with two files: -1. `../lite/nostr.bundle.js` - Official nostr-tools bundle -2. `../lite/nostr-lite.js` - NOSTR_LOGIN_LITE library with embedded NIP-46 extension - -## Architecture Update (2025-09-13) - -The library has been simplified from a three-file to a two-file architecture: -- โœ… **Before:** `nostr.bundle.js` + `nip46-extension.js` + `nostr-lite.js` -- โœ… **Now:** `nostr.bundle.js` + `nostr-lite.js` (with embedded NIP-46) - -All functionality remains identical - NIP-46 remote signing, all auth methods, and full compatibility are preserved. \ No newline at end of file diff --git a/examples/button.html b/examples/button.html new file mode 100644 index 0000000..7f9dd8d --- /dev/null +++ b/examples/button.html @@ -0,0 +1,75 @@ + + + + + + + Embedded NOSTR_LOGIN_LITE + + + + +
+
Login
+
+ + + + + + + + \ No newline at end of file diff --git a/examples/embedded.html b/examples/embedded.html index f72df97..3a7a87b 100644 --- a/examples/embedded.html +++ b/examples/embedded.html @@ -37,6 +37,7 @@ - + + \ No newline at end of file diff --git a/examples/modal-login-demo.html b/examples/modal-login-demo.html deleted file mode 100644 index 07c5a11..0000000 --- a/examples/modal-login-demo.html +++ /dev/null @@ -1,411 +0,0 @@ - - - - - - ๐Ÿ” NOSTR_LOGIN_LITE - Full Modal Login Demo - - - -
-

๐Ÿ” NOSTR_LOGIN_LITE Full Modal Login Demo

- -
-

๐Ÿ“š Available Login Methods

-

This demo showcases all login methods provided by NOSTR_LOGIN_LITE:

- -
-
-
๐Ÿ“ฑ
-

Extension Login

-

Use browser extensions like Alby, nos2x, or other Nostr-compatible extensions

-
-
-
๐Ÿ’พ
-

Local Account

-

Create and manage local Nostr keypairs stored in browser storage

-
-
-
๐Ÿ‘๏ธ
-

Read-Only Account

-

Access public content without authentication (limited functionality)

-
-
-
๐Ÿ”—
-

NIP-46 Remote

-

Connect to remote signers for secure key management

-
-
-
๐Ÿ”
-

OTP Backup

-

Secure local accounts with time-based one-time passwords

-
-
-
- - -
-

โš™๏ธ Library Status

-
Loading nostr-tools...
-
Loading NOSTR_LOGIN_LITE...
-
- - -
-

๐ŸŽฏ Launch Full Login Modal

-

Click the button below to launch the complete authentication modal with all available login options:

- - -
Ready to authenticate...
-
- The modal will show all available login methods based on your browser setup and library configuration. -
-
- - - - - -
-
- [Demo] Modal Login Demo initialized -
-
-
- - - - - - - - - - \ No newline at end of file diff --git a/examples/modal.html b/examples/modal.html new file mode 100644 index 0000000..28caf09 --- /dev/null +++ b/examples/modal.html @@ -0,0 +1,75 @@ + + + + + + + Embedded NOSTR_LOGIN_LITE + + + + + + + + + + + + + \ No newline at end of file diff --git a/nip46-test/README.md b/nip46-test/README.md deleted file mode 100644 index 7855d14..0000000 --- a/nip46-test/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# ๐Ÿฐ NIP-46 Remote Signer (Bunker) Test Setup - -This directory contains a complete NIP-46 remote signing setup for testing NOSTR_LOGIN_LITE. - -## ๐Ÿ”ง Setup Overview - -**Bunker**: A remote signer daemon that holds your private keys securely -**Client**: Browser client that connects to the bunker to request signatures -**NOSTR_LOGIN_LITE**: Connects to bunker for remote signing capability - -## ๐Ÿ”‘ Generated Keys - -``` -Bunker Secret Key: a33767c3bd05bda47880119d6665b79e6f0eecdf8d025966b0b59a9366379d01 -Bunker Public Key: 7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90 -``` - -## ๐Ÿš€ Testing NIP-46 Remote Signing - -### Step 1: Start the Bunker - -```bash -# Open a new terminal and run: -./nip46-test/start-bunker.sh -``` - -You'll see output like: -``` -๐Ÿ” Starting NIP-46 Bunker Remote Signer... -============================================== -Bunker Public Key: 7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90 -Secret key is securely held by bunker - -๐Ÿš€ Starting bunker daemon... -{"time":"202X-XX-XXTXX:XX:XX.XXXZ","level":"info","msg":"starting bunker on ws://localhost:8080"} -{"time":"202X-XX-XXTXX:XX:XX.XXXZ","level":"info","msg":"bunker ready to handle NIP-46 requests"} -``` - -### Step 2: Test with NOSTR_LOGIN_LITE - -Navigate to: -``` -http://localhost:8000/examples/modal-login-demo.html -``` - -Click "๐Ÿš€ Launch Authentication Modal" and select **"NIP-46 Remote"** option. - -The browser will connect to the bunker running on `ws://localhost:8080` and request signatures remotely. - -## ๐Ÿ”„ How NIP-46 Works - -``` -Browser (NOSTR_LOGIN_LITE) โ†’ WebSocket โ†’ Bunker (NAK on localhost:8080) - โ†“ โ†“ - Requests signature Holds private key - โ†“ โ†“ - Receives signed event Signs & returns result -``` - -## ๐Ÿ“ Files in this Directory - -- `start-bunker.sh` - Script to start the remote signer daemon -- `bunker-config.js` - Configuration for NOSTR_LOGIN_LITE -- `README.md` - This documentation - -## ๐Ÿงช Testing Scenarios - -### โœ… Successful Connection -- Bunker runs on localhost:8080 -- Browser connects and requests pubkey -- Bunker responds with `7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90` - -### ๐Ÿ”ง Signature Requests -- Browser sends event to sign -- Bunker signs with private key -- Signed event returned to browser -- Browser publishes signed event to relay - -### ๐Ÿ› Debug Issues -- Check bunker logs for connection errors -- Verify WebSocket connection in browser dev tools -- Look for NIP-46 protocol errors - -## ๐Ÿ“ NOSTR_LOGIN_LITE Configuration - -In your app, configure remote signing like this: - -```javascript -const nip46Config = { - type: "nip46", - bunker: { - pubkey: "7566048aa9df5b36428f2ce364797f7ac6f6d4a17ee566f0cd3fefcf35146b90", - url: "ws://your-bunker-server:8080" // Production URL - } -}; - -await window.NOSTR_LOGIN_LITE.init(nip46Config); -``` - -## โš ๏ธ Production Notes - -- **This setup uses localhost** - replace with real server URL in production -- **Private key is shown for testing** - production bunkers should be secured -- **WebSocket URL should be secure** (wss://) in production -- **Consider authentication** for your bunker to prevent unauthorized access - -## ๐ŸŽฏ Common Testing Commands - -### Check NAK version -```bash -nak --version -``` - -### Generate new keys (if needed) -```bash -nak key generate # Secret key -echo "your_secret_key_here" | nak key public # Public key -``` - -### Manual bunker test -```bash -nak bunker --sec "your_secret_key" --port 8080 --relay "wss://relay.damus.io" -``` - -## ๐Ÿ› Troubleshooting - -**Bunker won't start:** -- Check if port 8080 is free -- Verify NAK is installed correctly - -**Browser can't connect:** -- Check firewall settings -- Verify bunker is running (`ps aux | grep nak`) -- Check browser console for WebSocket errors - -**Signing fails:** -- Verify keys are correct -- Check bunker logs for errors -- Ensure event format is valid \ No newline at end of file diff --git a/nip46-test/start-bunker.sh b/start-bunker.sh similarity index 100% rename from nip46-test/start-bunker.sh rename to start-bunker.sh diff --git a/themes/README.md b/themes/README.md new file mode 100644 index 0000000..fc5d36b --- /dev/null +++ b/themes/README.md @@ -0,0 +1,370 @@ +# NOSTR_LOGIN_LITE Theme System + +A comprehensive theming system supporting CSS custom properties, JSON metadata, and runtime theme switching. + +## Architecture Overview + +The theme system consists of: + +- **CSS Custom Properties**: Dynamic styling variables +- **JSON Metadata**: Theme descriptions and configurations +- **Theme Manager**: Runtime loading and switching +- **Directory Organization**: Structured theme packages + +## Directory Structure + +``` +themes/ +โ”œโ”€โ”€ README.md # This documentation +โ”œโ”€โ”€ theme-manager.js # Theme management system +โ”œโ”€โ”€ default/ # Default monospace theme +โ”‚ โ”œโ”€โ”€ theme.json # Theme metadata +โ”‚ โ”œโ”€โ”€ theme.css # CSS custom properties +โ”‚ โ””โ”€โ”€ assets/ # Theme assets (fonts, images) +โ”œโ”€โ”€ dark/ # Dark cyberpunk theme +โ”‚ โ”œโ”€โ”€ theme.json +โ”‚ โ”œโ”€โ”€ theme.css +โ”‚ โ””โ”€โ”€ assets/ +โ””โ”€โ”€ community/ # Community contributed themes + โ””โ”€โ”€ [theme-name]/ + โ”œโ”€โ”€ theme.json + โ”œโ”€โ”€ theme.css + โ””โ”€โ”€ assets/ +``` + +## CSS Custom Properties + +All themes use standardized CSS custom properties with the `--nl-` prefix: + +### Colors +- `--nl-primary-color`: Main text/border color +- `--nl-secondary-color`: Background color +- `--nl-accent-color`: Hover/active accent color + +### Typography +- `--nl-font-family`: Base font family +- `--nl-font-size-base`: Base font size (14px) +- `--nl-font-size-title`: Title font size (24px) +- `--nl-font-size-heading`: Heading font size (18px) +- `--nl-font-size-button`: Button font size (16px) +- `--nl-font-weight-normal`: Normal weight (400) +- `--nl-font-weight-medium`: Medium weight (500) +- `--nl-font-weight-bold`: Bold weight (600) + +### Layout +- `--nl-border-radius`: Border radius (15px) +- `--nl-border-width`: Border thickness (3px) +- `--nl-border-style`: Border style (solid) +- `--nl-padding-button`: Button padding (12px 16px) +- `--nl-padding-container`: Container padding (20px 24px) + +### Effects +- `--nl-transition-duration`: Animation duration (0.2s) +- `--nl-transition-easing`: Animation easing (ease) +- `--nl-shadow`: Box shadow effects +- `--nl-backdrop-filter`: Backdrop filter effects + +### Component States +- `--nl-button-bg`: Button background +- `--nl-button-color`: Button text color +- `--nl-button-border`: Button border +- `--nl-button-hover-border-color`: Button hover border +- `--nl-button-active-bg`: Button active background +- `--nl-button-active-color`: Button active text + +## Theme Metadata (theme.json) + +Each theme must include a `theme.json` file with the following structure: + +```json +{ + "name": "Theme Display Name", + "version": "1.0.0", + "author": "Author Name/Email", + "description": "Theme description", + "preview": "preview.png", + "compatibility": "1.0+", + "license": "MIT", + "variables": { + "--nl-primary-color": "#000000", + "--nl-secondary-color": "#ffffff", + "--nl-accent-color": "#ff0000" + }, + "assets": ["fonts/", "images/"], + "tags": ["monospace", "dark", "accessibility"] +} +``` + +### Required Fields +- `name`: Human-readable theme name +- `version`: Semantic version number +- `variables`: CSS custom property values + +### Optional Fields +- `author`: Theme creator information +- `description`: Theme description +- `preview`: Preview image filename +- `compatibility`: Minimum library version +- `license`: License identifier (MIT, GPL, etc.) +- `assets`: Additional asset directories +- `tags`: Theme categorization tags + +## Creating a New Theme + +### 1. Create Theme Directory + +```bash +mkdir themes/my-theme +cd themes/my-theme +``` + +### 2. Create theme.json + +```json +{ + "name": "My Custom Theme", + "version": "1.0.0", + "author": "Your Name", + "description": "A custom theme for NOSTR_LOGIN_LITE", + "variables": { + "--nl-primary-color": "#your-color", + "--nl-secondary-color": "#your-bg-color", + "--nl-accent-color": "#your-accent-color", + "--nl-font-family": "\"Your Font\", monospace" + }, + "tags": ["custom"] +} +``` + +### 3. Create theme.css + +```css +:root { + /* Colors */ + --nl-primary-color: #your-color; + --nl-secondary-color: #your-bg-color; + --nl-accent-color: #your-accent-color; + + /* Typography */ + --nl-font-family: "Your Font", monospace; + + /* Layout - inherit defaults or customize */ + --nl-border-radius: 15px; + --nl-border-width: 3px; + + /* Add custom variables */ + --nl-custom-shadow: 0 0 10px rgba(0,0,0,0.3); +} + +/* Optional: Custom component styles */ +.nl-button { + /* Theme-specific enhancements */ + box-shadow: var(--nl-custom-shadow); +} +``` + +### 4. Add Assets (Optional) + +``` +my-theme/ +โ”œโ”€โ”€ theme.json +โ”œโ”€โ”€ theme.css +โ””โ”€โ”€ assets/ + โ”œโ”€โ”€ fonts/ + โ”‚ โ””โ”€โ”€ custom-font.woff2 + โ””โ”€โ”€ images/ + โ””โ”€โ”€ pattern.png +``` + +### 5. Register Theme + +Update `theme-manager.js` to include your theme: + +```javascript +this.availableThemes.set('my-theme', { + name: 'My Custom Theme', + path: 'my-theme', + description: 'A custom theme for NOSTR_LOGIN_LITE' +}); +``` + +## Using Themes + +### Initialization + +```javascript +await NOSTR_LOGIN_LITE.init({ + theme: 'default', + themePath: './themes/' +}); +``` + +### Runtime Switching + +```javascript +// Switch theme +await NOSTR_LOGIN_LITE.switchTheme('dark'); + +// Get current theme +const current = NOSTR_LOGIN_LITE.getCurrentTheme(); + +// List available themes +const available = NOSTR_LOGIN_LITE.getAvailableThemes(); +``` + +### Custom Variables + +```javascript +// Set custom variable +NOSTR_LOGIN_LITE.setThemeVariable('--nl-accent-color', '#ff00ff'); + +// Get variable value +const value = NOSTR_LOGIN_LITE.getThemeVariable('--nl-accent-color'); +``` + +### Theme Export + +```javascript +// Export current theme configuration +const themeData = NOSTR_LOGIN_LITE.exportTheme(); +console.log(JSON.stringify(themeData, null, 2)); +``` + +## Theme Guidelines + +### Accessibility +- Ensure sufficient color contrast (4.5:1 minimum) +- Test with screen readers +- Support high contrast mode +- Use semantic color names + +### Performance +- Minimize CSS file size +- Optimize asset files +- Use web-safe fonts as fallbacks +- Consider loading performance + +### Compatibility +- Test across browsers +- Ensure mobile responsiveness +- Validate CSS custom property support +- Test with different font sizes + +### Best Practices +- Use consistent naming conventions +- Provide clear documentation +- Include preview images +- Tag themes appropriately +- Test thoroughly before submission + +## Community Contributions + +### Submission Process +1. Fork the repository +2. Create theme in `themes/community/your-theme/` +3. Follow all guidelines above +4. Test thoroughly +5. Submit pull request with: + - Theme files + - Preview screenshot + - Documentation updates + +### Review Criteria +- Code quality and organization +- Accessibility compliance +- Cross-browser compatibility +- Unique design contribution +- Proper documentation + +## Built-in Themes + +### Default Theme +- **Colors**: Black/white/red +- **Typography**: Courier New monospace +- **Style**: Clean, minimalist, accessible +- **Use Case**: General purpose, high readability + +### Dark Theme +- **Colors**: Green/black/magenta +- **Typography**: Courier New monospace +- **Style**: Cyberpunk, terminal-inspired +- **Use Case**: Low light environments, developer aesthetic + +## API Reference + +### ThemeManager Class + +```javascript +const themeManager = new NostrThemeManager(); + +// Load theme +await themeManager.loadTheme('theme-name'); + +// Switch theme +await themeManager.switchTheme('theme-name'); + +// Get available themes +const themes = themeManager.getAvailableThemes(); + +// Set/get variables +themeManager.setThemeVariable('--nl-accent-color', '#ff0000'); +const value = themeManager.getThemeVariable('--nl-accent-color'); + +// Export current theme +const exported = themeManager.exportCurrentTheme(); +``` + +### NOSTR_LOGIN_LITE Integration + +```javascript +// Initialize with theme +await NOSTR_LOGIN_LITE.init({ theme: 'dark' }); + +// Theme management +await NOSTR_LOGIN_LITE.switchTheme('theme-name'); +const current = NOSTR_LOGIN_LITE.getCurrentTheme(); +const available = NOSTR_LOGIN_LITE.getAvailableThemes(); + +// Variable management +NOSTR_LOGIN_LITE.setThemeVariable('--nl-primary-color', '#000000'); +const color = NOSTR_LOGIN_LITE.getThemeVariable('--nl-primary-color'); + +// Export functionality +const themeData = NOSTR_LOGIN_LITE.exportTheme(); +``` + +## Events + +The theme system dispatches events for integration: + +```javascript +// Theme change event +window.addEventListener('nlThemeChanged', (event) => { + console.log('New theme:', event.detail.theme); + console.log('Theme data:', event.detail.data); +}); +``` + +## Troubleshooting + +### Theme Not Loading +- Check theme.json syntax +- Verify file paths +- Check browser console for errors +- Ensure CSS custom properties are supported + +### Variables Not Applying +- Verify CSS custom property names (--nl- prefix) +- Check CSS specificity +- Ensure theme CSS is loaded after base styles +- Validate variable values + +### Performance Issues +- Optimize CSS file size +- Compress assets +- Use efficient selectors +- Consider lazy loading for large themes + +## License + +The theme system is open source under the MIT license. Individual themes may have their own licenses as specified in their theme.json files. \ No newline at end of file diff --git a/themes/dark/theme.css b/themes/dark/theme.css new file mode 100644 index 0000000..530d9a0 --- /dev/null +++ b/themes/dark/theme.css @@ -0,0 +1,115 @@ +/** + * NOSTR_LOGIN_LITE - Dark Monospace Theme + */ + +:root { + /* Core Variables (6) */ + --nl-primary-color: #white; + --nl-secondary-color: #black; + --nl-accent-color: #ff0000; + --nl-muted-color: #666666; + --nl-font-family: "Courier New", Courier, monospace; + --nl-border-radius: 15px; + --nl-border-width: 3px; + + /* Floating Tab Variables (8) */ + --nl-tab-bg-logged-out: #ffffff; + --nl-tab-bg-logged-in: #000000; + --nl-tab-bg-opacity-logged-out: 0.9; + --nl-tab-bg-opacity-logged-in: 0.8; + --nl-tab-color-logged-out: #000000; + --nl-tab-color-logged-in: #ffffff; + --nl-tab-border-logged-out: #000000; + --nl-tab-border-logged-in: #ff0000; + --nl-tab-border-opacity-logged-out: 1.0; + --nl-tab-border-opacity-logged-in: 0.9; +} + +/* Base component styles using simplified variables */ +.nl-component { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +.nl-button { + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + font-family: var(--nl-font-family); + cursor: pointer; + transition: all 0.2s ease; +} + +.nl-button:hover { + border-color: var(--nl-accent-color); +} + +.nl-button:active { + background: var(--nl-accent-color); + color: var(--nl-secondary-color); +} + +.nl-input { + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + font-family: var(--nl-font-family); + box-sizing: border-box; +} + +.nl-input:focus { + border-color: var(--nl-accent-color); + outline: none; +} + +.nl-container { + background: var(--nl-secondary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); +} + +.nl-title, .nl-heading { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); + margin: 0; +} + +.nl-text { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +.nl-text--muted { + color: var(--nl-muted-color); +} + +.nl-icon { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +/* Floating tab styles */ +.nl-floating-tab { + font-family: var(--nl-font-family); + border-radius: var(--nl-border-radius); + border: var(--nl-border-width) solid; + transition: all 0.2s ease; +} + +.nl-floating-tab--logged-out { + background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out)); + color: var(--nl-tab-color-logged-out); + border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out)); +} + +.nl-floating-tab--logged-in { + background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in)); + color: var(--nl-tab-color-logged-in); + border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in)); +} + +.nl-transition { + transition: all 0.2s ease; +} \ No newline at end of file diff --git a/themes/default/theme.css b/themes/default/theme.css new file mode 100644 index 0000000..3c6561b --- /dev/null +++ b/themes/default/theme.css @@ -0,0 +1,117 @@ +/** + * NOSTR_LOGIN_LITE - Default Monospace Theme + * Black/white/red color scheme with monospace typography + * Simplified 14-variable system (6 core + 8 floating tab) + */ + +:root { + /* Core Variables (6) */ + --nl-primary-color: #000000; + --nl-secondary-color: #ffffff; + --nl-accent-color: #ff0000; + --nl-muted-color: #666666; + --nl-font-family: "Courier New", Courier, monospace; + --nl-border-radius: 15px; + --nl-border-width: 3px; + + /* Floating Tab Variables (8) */ + --nl-tab-bg-logged-out: #ffffff; + --nl-tab-bg-logged-in: #ffffff; + --nl-tab-bg-opacity-logged-out: 0.9; + --nl-tab-bg-opacity-logged-in: 0.2; + --nl-tab-color-logged-out: #000000; + --nl-tab-color-logged-in: #ffffff; + --nl-tab-border-logged-out: #000000; + --nl-tab-border-logged-in: #ff0000; + --nl-tab-border-opacity-logged-out: 1.0; + --nl-tab-border-opacity-logged-in: 0.1; +} + +/* Base component styles using simplified variables */ +.nl-component { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +.nl-button { + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + font-family: var(--nl-font-family); + cursor: pointer; + transition: all 0.2s ease; +} + +.nl-button:hover { + border-color: var(--nl-accent-color); +} + +.nl-button:active { + background: var(--nl-accent-color); + color: var(--nl-secondary-color); +} + +.nl-input { + background: var(--nl-secondary-color); + color: var(--nl-primary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); + font-family: var(--nl-font-family); + box-sizing: border-box; +} + +.nl-input:focus { + border-color: var(--nl-accent-color); + outline: none; +} + +.nl-container { + background: var(--nl-secondary-color); + border: var(--nl-border-width) solid var(--nl-primary-color); + border-radius: var(--nl-border-radius); +} + +.nl-title, .nl-heading { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); + margin: 0; +} + +.nl-text { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +.nl-text--muted { + color: var(--nl-muted-color); +} + +.nl-icon { + font-family: var(--nl-font-family); + color: var(--nl-primary-color); +} + +/* Floating tab styles */ +.nl-floating-tab { + font-family: var(--nl-font-family); + border-radius: var(--nl-border-radius); + border: var(--nl-border-width) solid; + transition: all 0.2s ease; +} + +.nl-floating-tab--logged-out { + background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out)); + color: var(--nl-tab-color-logged-out); + border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out)); +} + +.nl-floating-tab--logged-in { + background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in)); + color: var(--nl-tab-color-logged-in); + border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in)); +} + +.nl-transition { + transition: all 0.2s ease; +} \ No newline at end of file diff --git a/themes/index.json b/themes/index.json new file mode 100644 index 0000000..3015965 --- /dev/null +++ b/themes/index.json @@ -0,0 +1,35 @@ +{ + "version": "1.0.0", + "themes": { + "default": { + "name": "Default Monospace", + "path": "default", + "description": "Black/white/red monospace theme with rounded buttons", + "author": "NOSTR_LOGIN_LITE", + "version": "1.0.0", + "preview": "default/preview.png", + "tags": ["monospace", "minimalist", "accessibility", "default"], + "featured": true + }, + "dark": { + "name": "Dark Monospace", + "path": "dark", + "description": "Dark mode with green accents and monospace typography", + "author": "NOSTR_LOGIN_LITE", + "version": "1.0.0", + "preview": "dark/preview.png", + "tags": ["dark", "cyberpunk", "monospace", "accessibility"], + "featured": true + } + }, + "categories": { + "official": ["default", "dark"], + "community": [], + "experimental": [] + }, + "metadata": { + "total_themes": 2, + "last_updated": "2025-01-14T11:13:00.000Z", + "schema_version": "1.0.0" + } +} \ No newline at end of file diff --git a/themes/theme-manager.js b/themes/theme-manager.js new file mode 100644 index 0000000..d4d44a5 --- /dev/null +++ b/themes/theme-manager.js @@ -0,0 +1,286 @@ +/** + * NOSTR_LOGIN_LITE Theme Manager + * Handles theme loading, switching, and CSS custom property management + */ + +class NostrThemeManager { + constructor() { + this.currentTheme = null; + this.availableThemes = new Map(); + this.themeCache = new Map(); + this.basePath = './themes/'; + + // Initialize with default theme + this.init(); + } + + async init() { + try { + // Load available themes index + await this.loadThemeIndex(); + + // Set default theme if none is set + if (!this.currentTheme) { + await this.loadTheme('default'); + } + + console.log('NostrThemeManager: Initialized with themes:', Array.from(this.availableThemes.keys())); + } catch (error) { + console.error('NostrThemeManager: Initialization failed:', error); + this.fallbackToInlineStyles(); + } + } + + async loadThemeIndex() { + // For now, we'll manually register available themes + // In production, this could fetch from a themes.json index file + this.availableThemes.set('default', { + name: 'Default Monospace', + path: 'default', + description: 'Black/white/red monospace theme' + }); + + this.availableThemes.set('dark', { + name: 'Dark Monospace', + path: 'dark', + description: 'Dark mode with green accents and monospace typography' + }); + + // Future themes can be registered here or loaded from an index + // this.availableThemes.set('cyberpunk', { ... }); + } + + async loadTheme(themeName) { + try { + console.log(`NostrThemeManager: Loading theme "${themeName}"`); + + // Check if theme exists + if (!this.availableThemes.has(themeName)) { + throw new Error(`Theme "${themeName}" not found`); + } + + // Check cache first + if (this.themeCache.has(themeName)) { + const cachedTheme = this.themeCache.get(themeName); + this.applyTheme(cachedTheme); + this.currentTheme = themeName; + return cachedTheme; + } + + // Load theme metadata + const themeInfo = this.availableThemes.get(themeName); + const metadataUrl = `${this.basePath}${themeInfo.path}/theme.json`; + + const response = await fetch(metadataUrl); + if (!response.ok) { + throw new Error(`Failed to load theme metadata: ${response.statusText}`); + } + + const themeData = await response.json(); + + // Validate theme data + this.validateThemeData(themeData); + + // Load CSS file + await this.loadThemeCSS(themeInfo.path); + + // Cache the theme data + this.themeCache.set(themeName, themeData); + + // Apply the theme + this.applyTheme(themeData); + this.currentTheme = themeName; + + console.log(`NostrThemeManager: Successfully loaded theme "${themeName}"`); + + // Dispatch theme change event + this.dispatchThemeChangeEvent(themeName, themeData); + + return themeData; + + } catch (error) { + console.error(`NostrThemeManager: Failed to load theme "${themeName}":`, error); + throw error; + } + } + + async loadThemeCSS(themePath) { + const cssUrl = `${this.basePath}${themePath}/theme.css`; + + // Remove existing theme CSS + const existingThemeCSS = document.getElementById('nl-theme-css'); + if (existingThemeCSS) { + existingThemeCSS.remove(); + } + + // Load new theme CSS + const link = document.createElement('link'); + link.id = 'nl-theme-css'; + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = cssUrl; + + // Wait for CSS to load + await new Promise((resolve, reject) => { + link.onload = resolve; + link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`)); + document.head.appendChild(link); + }); + } + + applyTheme(themeData) { + if (!themeData.variables) { + console.warn('NostrThemeManager: Theme data has no variables to apply'); + return; + } + + const root = document.documentElement; + + // Apply CSS custom properties + Object.entries(themeData.variables).forEach(([property, value]) => { + root.style.setProperty(property, value); + }); + + console.log(`NostrThemeManager: Applied ${Object.keys(themeData.variables).length} CSS variables`); + } + + validateThemeData(themeData) { + const required = ['name', 'version', 'variables']; + + for (const field of required) { + if (!themeData[field]) { + throw new Error(`Theme validation failed: missing required field "${field}"`); + } + } + + if (typeof themeData.variables !== 'object') { + throw new Error('Theme validation failed: variables must be an object'); + } + } + + fallbackToInlineStyles() { + console.log('NostrThemeManager: Falling back to inline styles'); + + // Apply default theme variables directly + const defaultVariables = { + '--nl-primary-color': '#000000', + '--nl-secondary-color': '#ffffff', + '--nl-accent-color': '#ff0000', + '--nl-font-family': '"Courier New", Courier, monospace', + '--nl-border-radius': '15px', + '--nl-border-width': '3px', + '--nl-border-style': 'solid', + '--nl-padding-button': '12px 16px', + '--nl-padding-container': '20px 24px', + '--nl-font-size-base': '14px', + '--nl-font-size-title': '24px', + '--nl-font-size-button': '16px', + '--nl-transition-duration': '0.2s' + }; + + const root = document.documentElement; + Object.entries(defaultVariables).forEach(([property, value]) => { + root.style.setProperty(property, value); + }); + + this.currentTheme = 'fallback'; + } + + dispatchThemeChangeEvent(themeName, themeData) { + if (typeof window !== 'undefined') { + const event = new CustomEvent('nlThemeChanged', { + detail: { + theme: themeName, + data: themeData, + timestamp: Date.now() + } + }); + window.dispatchEvent(event); + } + } + + // Public API methods + getCurrentTheme() { + return this.currentTheme; + } + + getAvailableThemes() { + return Array.from(this.availableThemes.keys()); + } + + getThemeInfo(themeName) { + return this.availableThemes.get(themeName); + } + + async switchTheme(themeName) { + return await this.loadTheme(themeName); + } + + getThemeVariable(variableName) { + if (typeof window === 'undefined') return null; + + const root = document.documentElement; + const style = getComputedStyle(root); + return style.getPropertyValue(variableName); + } + + setThemeVariable(variableName, value) { + if (typeof window === 'undefined') return; + + const root = document.documentElement; + root.style.setProperty(variableName, value); + } + + resetTheme() { + const root = document.documentElement; + + // Remove all nl- prefixed custom properties + const style = getComputedStyle(root); + for (let i = 0; i < style.length; i++) { + const property = style[i]; + if (property.startsWith('--nl-')) { + root.style.removeProperty(property); + } + } + + // Remove theme CSS + const themeCSS = document.getElementById('nl-theme-css'); + if (themeCSS) { + themeCSS.remove(); + } + + this.currentTheme = null; + } + + // Theme creation utilities (for developers) + exportCurrentTheme() { + const root = document.documentElement; + const style = getComputedStyle(root); + const variables = {}; + + for (let i = 0; i < style.length; i++) { + const property = style[i]; + if (property.startsWith('--nl-')) { + variables[property] = style.getPropertyValue(property); + } + } + + return { + name: 'Custom Theme', + version: '1.0.0', + author: 'User', + description: 'Exported theme', + variables, + timestamp: new Date().toISOString() + }; + } +} + +// Export for use in NOSTR_LOGIN_LITE +if (typeof window !== 'undefined') { + window.NostrThemeManager = NostrThemeManager; + console.log('NostrThemeManager: Class available globally'); +} else { + // Node.js environment + module.exports = NostrThemeManager; +} \ No newline at end of file