This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tools.pass: add dummy se plugin for linux hosts

+187 -9
+171
tools/pass/age-plugin-se-dummy.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Dummy age-plugin-se for Linux. 4 + 5 + This plugin allows encrypting secrets to Secure Enclave recipients (age1se... addresses) 6 + from Linux machines. The encrypted files can then be decrypted on macOS using the real 7 + age-plugin-se with hardware Secure Enclave support. 8 + 9 + The crypto is identical to the real plugin - only the key storage differs: 10 + - Real plugin: Private keys stored in Apple Secure Enclave hardware 11 + - This plugin: Only supports encryption (no private key storage) 12 + """ 13 + 14 + import sys 15 + import io 16 + import base64 17 + import hashlib 18 + from cryptography.hazmat.primitives.asymmetric import ec 19 + from cryptography.hazmat.primitives import hashes, serialization 20 + from cryptography.hazmat.primitives.kdf.hkdf import HKDF 21 + from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 22 + 23 + 24 + def bech32_decode(s): 25 + """Simple bech32 decode for age1se addresses.""" 26 + charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 27 + hrp_end = s.rfind('1') 28 + if hrp_end < 1: 29 + return None 30 + data = s[hrp_end + 1:] 31 + decoded = [] 32 + for c in data: 33 + if c not in charset: 34 + return None 35 + decoded.append(charset.index(c)) 36 + # Convert from 5-bit to 8-bit groups 37 + bits = 0 38 + value = 0 39 + result = [] 40 + for d in decoded[:-6]: # Skip checksum 41 + value = (value << 5) | d 42 + bits += 5 43 + if bits >= 8: 44 + bits -= 8 45 + result.append((value >> bits) & 0xff) 46 + value &= (1 << bits) - 1 47 + return bytes(result) 48 + 49 + 50 + def write_stdout(s): 51 + """Write to stdout with explicit flush (unbuffered).""" 52 + stdout_raw.write(s.encode('utf-8')) 53 + stdout_raw.flush() 54 + 55 + 56 + def handle_recipient_v1(): 57 + """Handle the age plugin recipient-v1 protocol.""" 58 + file_keys = [] 59 + recipients = [] 60 + got_done = False 61 + 62 + # Read commands until -> done 63 + while True: 64 + line = sys.stdin.readline() 65 + if not line: # EOF 66 + break 67 + line = line.strip() 68 + 69 + if line.startswith("-> add-recipient "): 70 + recipients.append(line[17:]) 71 + elif line == "-> wrap-file-key": 72 + # Read body lines until empty line OR next command 73 + body_parts = [] 74 + while True: 75 + body_line = sys.stdin.readline() 76 + if not body_line: 77 + break 78 + body_line_stripped = body_line.strip() 79 + if body_line_stripped == "": 80 + break # Empty line terminates body 81 + if body_line_stripped.startswith("->"): 82 + # Next command encountered 83 + if body_line_stripped == "-> done": 84 + got_done = True 85 + break 86 + body_parts.append(body_line_stripped) 87 + file_keys.append("".join(body_parts)) 88 + if got_done: 89 + break 90 + elif line == "-> done": 91 + got_done = True 92 + break 93 + # Ignore other lines (grease commands, extension-labels, etc.) 94 + 95 + if not got_done: 96 + sys.exit(1) 97 + 98 + # Process each file key for each recipient 99 + for file_idx, file_key_b64 in enumerate(file_keys): 100 + for recipient in recipients: 101 + try: 102 + # Decode recipient public key (compressed P-256, 33 bytes) 103 + pubkey_bytes = bech32_decode(recipient) 104 + if not pubkey_bytes: 105 + raise ValueError("Failed to decode recipient") 106 + 107 + # Generate tag from public key hash (first 4 bytes of SHA256) 108 + tag = base64.b64encode( 109 + hashlib.sha256(pubkey_bytes).digest()[:4] 110 + ).decode().rstrip('=') 111 + 112 + # Decode file key (add padding for base64) 113 + padding = (4 - len(file_key_b64) % 4) % 4 114 + file_key = base64.b64decode(file_key_b64 + '=' * padding) 115 + 116 + # Load P-256 public key 117 + recipient_pubkey = ec.EllipticCurvePublicKey.from_encoded_point( 118 + ec.SECP256R1(), pubkey_bytes 119 + ) 120 + 121 + # Generate ephemeral key pair 122 + ephemeral_private = ec.generate_private_key(ec.SECP256R1()) 123 + ephemeral_public = ephemeral_private.public_key() 124 + ephemeral_bytes = ephemeral_public.public_bytes( 125 + serialization.Encoding.X962, 126 + serialization.PublicFormat.CompressedPoint 127 + ) 128 + 129 + # ECDH shared secret 130 + shared = ephemeral_private.exchange(ec.ECDH(), recipient_pubkey) 131 + 132 + # HKDF to derive wrapping key (matches age-plugin-se format) 133 + # salt = ephemeral || recipient_pubkey, info = "piv-p256" 134 + hkdf_salt = ephemeral_bytes + pubkey_bytes 135 + wrapping_key = HKDF( 136 + algorithm=hashes.SHA256(), 137 + length=32, 138 + salt=hkdf_salt, 139 + info=b"piv-p256", 140 + ).derive(shared) 141 + 142 + # Encrypt with ChaCha20-Poly1305 (zero nonce, no AAD) 143 + cipher = ChaCha20Poly1305(wrapping_key) 144 + encrypted = cipher.encrypt(b'\x00' * 12, file_key, None) 145 + 146 + # Format output (base64 without padding) 147 + ephemeral_b64 = base64.b64encode(ephemeral_bytes).decode().rstrip('=') 148 + encrypted_b64 = base64.b64encode(encrypted).decode().rstrip('=') 149 + 150 + # Output stanza (header + body) 151 + header = f"-> recipient-stanza {file_idx} piv-p256 {tag} {ephemeral_b64}" 152 + write_stdout(header + '\n') 153 + write_stdout(encrypted_b64 + '\n') 154 + 155 + except Exception: 156 + pass # Silently skip failed recipients 157 + 158 + # Signal completion with empty line after done (required by plugin protocol) 159 + write_stdout("-> done\n\n") 160 + sys.exit(0) 161 + 162 + 163 + if __name__ == "__main__": 164 + # Use unbuffered stdout for reliable plugin communication 165 + stdout_raw = io.FileIO(sys.stdout.fileno(), mode='w', closefd=False) 166 + 167 + if len(sys.argv) > 1 and sys.argv[1] == '--age-plugin=recipient-v1': 168 + handle_recipient_v1() 169 + else: 170 + # Unknown mode 171 + sys.exit(1)
+16 -9
tools/pass/flake-module.nix
··· 10 10 packages.pass = self.wrapLib.makeWrapper { 11 11 pkgs = pkgs; 12 12 package = pkgs.passage; 13 - runtimeInputs = [ 14 - pkgs.age-plugin-fido2-hmac 15 - pkgs.age-plugin-yubikey 16 - self.packages.${system}.pass-otp 17 - self.packages.${system}.age-detect 18 - ] 19 - ++ (pkgs.lib.optionals pkgs.stdenv.isDarwin [ 20 - pkgs.age-plugin-se 21 - ]); 13 + runtimeInputs = 14 + [ 15 + pkgs.age-plugin-fido2-hmac 16 + pkgs.age-plugin-yubikey 17 + self.packages.${system}.pass-otp 18 + self.packages.${system}.age-detect 19 + ] 20 + ++ (pkgs.lib.optionals pkgs.stdenv.isDarwin [ 21 + pkgs.age-plugin-se 22 + ]) 23 + ++ (pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ 24 + # Dummy age-plugin-se for Linux (encryption only, no Secure Enclave hardware) 25 + (pkgs.writeShellScriptBin "age-plugin-se" '' 26 + exec env PYTHONUNBUFFERED=1 ${pkgs.python3.withPackages (ps: [ ps.cryptography ])}/bin/python3 ${./age-plugin-se-dummy.py} "$@" 27 + '') 28 + ]); 22 29 aliases = [ "pass" ]; 23 30 wrapper = 24 31 {