ecdat 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. ecdat/__init__.py +10 -0
  2. ecdat/__main__.py +5 -0
  3. ecdat/cli/__init__.py +203 -0
  4. ecdat/cli/commands/__init__.py +0 -0
  5. ecdat/cli/commands/about.py +130 -0
  6. ecdat/cli/commands/demo.py +116 -0
  7. ecdat/cli/commands/doctor.py +296 -0
  8. ecdat/cli/commands/help_cmd.py +205 -0
  9. ecdat/cli/commands/scan.py +228 -0
  10. ecdat/cli/commands/version_cmd.py +48 -0
  11. ecdat/cli/parser.py +87 -0
  12. ecdat/demo_project/auth/login.py +75 -0
  13. ecdat/demo_project/certs/cert_verify.go +81 -0
  14. ecdat/demo_project/keyexchange/channel.go +48 -0
  15. ecdat/demo_project/legacy/LegacyCrypto.java +78 -0
  16. ecdat/demo_project/payments/payment.py +64 -0
  17. ecdat/demo_project/quantum/pqc_utils.py +67 -0
  18. ecdat/demo_project/quantum/slh_signer.py +40 -0
  19. ecdat/demo_project/tokens/signing.js +54 -0
  20. ecdat/py.typed +0 -0
  21. ecdat/services/__init__.py +1 -0
  22. ecdat/services/crashlog.py +109 -0
  23. ecdat/services/demo.py +85 -0
  24. ecdat/services/paths.py +52 -0
  25. ecdat/services/scanner.py +248 -0
  26. ecdat/services/viewmodel.py +326 -0
  27. ecdat/ui/__init__.py +1 -0
  28. ecdat/ui/art3d.py +136 -0
  29. ecdat/ui/art_static.py +65 -0
  30. ecdat/ui/art_text.py +81 -0
  31. ecdat/ui/banner.py +148 -0
  32. ecdat/ui/console.py +119 -0
  33. ecdat/ui/motion.py +64 -0
  34. ecdat/ui/render.py +486 -0
  35. ecdat/ui/theme.py +173 -0
  36. ecdat-0.2.0.dist-info/METADATA +142 -0
  37. ecdat-0.2.0.dist-info/RECORD +51 -0
  38. ecdat-0.2.0.dist-info/WHEEL +5 -0
  39. ecdat-0.2.0.dist-info/entry_points.txt +2 -0
  40. ecdat-0.2.0.dist-info/licenses/LICENSE +21 -0
  41. ecdat-0.2.0.dist-info/top_level.txt +2 -0
  42. ecdat_core/__init__.py +6 -0
  43. ecdat_core/cbom_export.py +287 -0
  44. ecdat_core/cli.py +202 -0
  45. ecdat_core/detector.py +273 -0
  46. ecdat_core/ingestion.py +581 -0
  47. ecdat_core/models.py +145 -0
  48. ecdat_core/recommender.py +74 -0
  49. ecdat_core/risk_engine.py +264 -0
  50. ecdat_core/signature_loader.py +204 -0
  51. ecdat_core/signatures.json +692 -0
@@ -0,0 +1,228 @@
1
+ """``ecdat scan`` — run a scan and render/emit the result.
2
+
3
+ The command module is deliberately thin at import time: Rich, the scanner
4
+ engine, and the renderers are imported inside :func:`run` so that
5
+ ``import ecdat.cli`` stays fast and never pulls in UI frameworks.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING: # pragma: no cover - typing only
17
+ from ecdat.services.viewmodel import ScanVM
18
+ from ecdat_core.models import ScanResult
19
+
20
+ NAME = "scan"
21
+
22
+ _FORMATS = ("pretty", "json", "cbom", "summary")
23
+
24
+ # Most-severe first. "quantum-safe" is intentionally absent: a quantum-safe
25
+ # finding must never trip --fail-on.
26
+ _SEVERITY = ("critical", "high", "medium", "low")
27
+
28
+
29
+ def register(subparsers: "argparse._SubParsersAction") -> None:
30
+ """Attach the ``scan`` subcommand to *subparsers*."""
31
+ parser = subparsers.add_parser(
32
+ "scan",
33
+ help="Scan a local directory or git repository",
34
+ description=(
35
+ "Scan a directory (or an https:// Git repository) for "
36
+ "cryptographic artefacts and report their post-quantum risk."
37
+ ),
38
+ )
39
+ parser.add_argument(
40
+ "target",
41
+ nargs="?",
42
+ default=".",
43
+ help="Directory to scan, or a Git URL with --git-url (default: .)",
44
+ )
45
+ parser.add_argument(
46
+ "--git-url",
47
+ action="store_true",
48
+ help="Treat TARGET as an https:// Git repository to clone and scan",
49
+ )
50
+ parser.add_argument(
51
+ "-f",
52
+ "--format",
53
+ choices=_FORMATS,
54
+ default="pretty",
55
+ help="Output format (default: pretty)",
56
+ )
57
+ parser.add_argument(
58
+ "-o",
59
+ "--output",
60
+ metavar="DIR",
61
+ default=None,
62
+ help="Also write cbom.json and summary.json into DIR",
63
+ )
64
+ parser.add_argument(
65
+ "--fail-on",
66
+ choices=_SEVERITY,
67
+ default=None,
68
+ metavar="LEVEL",
69
+ help="Exit 1 when a finding is at or above LEVEL "
70
+ "(critical|high|medium|low); quantum-safe findings never count",
71
+ )
72
+ parser.add_argument(
73
+ "--no-progress",
74
+ action="store_true",
75
+ help="Never show the progress spinner",
76
+ )
77
+ parser.add_argument(
78
+ "-q",
79
+ "--quiet",
80
+ action="store_true",
81
+ help="Suppress banner, progress, and status messages",
82
+ )
83
+ parser.add_argument(
84
+ "--limit",
85
+ type=int,
86
+ default=15,
87
+ metavar="N",
88
+ help="Maximum findings shown in the pretty table (default: 15)",
89
+ )
90
+
91
+
92
+ def run(args) -> int:
93
+ """Execute a scan for the parsed *args*; return the process exit code.
94
+
95
+ Raises:
96
+ ScanError: Propagated to :func:`ecdat.cli.main`, which maps it to the
97
+ exit code carried on the exception. ``BrokenPipeError`` and
98
+ ``KeyboardInterrupt`` likewise propagate to ``main``.
99
+ """
100
+ from rich.text import Text
101
+
102
+ from ecdat.services.scanner import classify_target, perform_scan
103
+ from ecdat.ui.console import make_console
104
+
105
+ no_color = True if getattr(args, "no_color", False) else None
106
+ out_console = make_console(no_color=no_color)
107
+ err_console = make_console(stderr=True, no_color=no_color)
108
+
109
+ quiet = bool(getattr(args, "quiet", False))
110
+ target = classify_target(args.target, force_git=args.git_url)
111
+
112
+ show_progress = (
113
+ not quiet
114
+ and not getattr(args, "no_progress", False)
115
+ and err_console.is_terminal
116
+ )
117
+
118
+ if show_progress:
119
+ with err_console.status("Scanning \u2026", spinner="dots"):
120
+ outcome = perform_scan(target, force_git=args.git_url)
121
+ else:
122
+ outcome = perform_scan(target, force_git=args.git_url)
123
+
124
+ result = outcome.result
125
+ vm = outcome.vm
126
+
127
+ if args.output:
128
+ _write_outputs(Path(args.output), result, err_console, quiet)
129
+
130
+ output_format = args.format
131
+ if output_format == "json":
132
+ _emit_json(result.model_dump(mode="json"))
133
+ elif output_format == "cbom":
134
+ from ecdat_core.cbom_export import export_cbom
135
+
136
+ _emit_json(export_cbom(result))
137
+ elif output_format == "summary":
138
+ from ecdat_core.cbom_export import export_summary
139
+
140
+ _emit_json(export_summary(result))
141
+ else:
142
+ _render_pretty(out_console, vm, limit=args.limit, quiet=quiet)
143
+
144
+ if vm.total == 0 and not quiet:
145
+ err_console.print(
146
+ Text(
147
+ f"Warning: no cryptographic artefacts detected in "
148
+ f"{vm.files_scanned} files.",
149
+ style="yellow",
150
+ ),
151
+ )
152
+
153
+ if args.fail_on is not None and _fails_at_or_above(vm, args.fail_on):
154
+ return 1
155
+ return 0
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Output helpers
160
+ # ---------------------------------------------------------------------------
161
+
162
+
163
+ def _emit_json(payload: dict) -> None:
164
+ """Write *payload* as pretty JSON to stdout — the ONLY thing on stdout."""
165
+ sys.stdout.write(json.dumps(payload, indent=2) + "\n")
166
+
167
+
168
+ def _write_outputs(directory: Path, result, err_console, quiet: bool) -> None:
169
+ """Write ``cbom.json`` and ``summary.json`` into *directory* (UTF-8)."""
170
+ from ecdat.services.scanner import ScanError
171
+ from ecdat_core.cbom_export import export_cbom, export_summary
172
+
173
+ try:
174
+ directory.mkdir(parents=True, exist_ok=True)
175
+ except OSError as exc:
176
+ raise ScanError(
177
+ f"Could not create output directory {directory}: {exc}",
178
+ exit_code=3,
179
+ ) from exc
180
+
181
+ payloads = (
182
+ ("cbom.json", export_cbom(result)),
183
+ ("summary.json", export_summary(result)),
184
+ )
185
+ for filename, payload in payloads:
186
+ path = directory / filename
187
+ try:
188
+ path.write_text(
189
+ json.dumps(payload, indent=2, sort_keys=True) + "\n",
190
+ encoding="utf-8",
191
+ )
192
+ except OSError as exc:
193
+ raise ScanError(
194
+ f"Could not write {path}: {exc}",
195
+ exit_code=3,
196
+ ) from exc
197
+ if not quiet:
198
+ err_console.print(f"Wrote {path}")
199
+
200
+
201
+ def _render_pretty(
202
+ console,
203
+ vm: "ScanVM",
204
+ *,
205
+ limit: int,
206
+ quiet: bool,
207
+ ) -> None:
208
+ """Render the banner (unless quiet) and the scan report to stdout."""
209
+ from ecdat import __version__
210
+ from ecdat.ui.banner import render_banner
211
+ from ecdat.ui.render import scan_report
212
+
213
+ if not quiet:
214
+ console.print(render_banner(version=__version__))
215
+ console.print(scan_report(vm, limit=limit))
216
+
217
+
218
+ def _fails_at_or_above(vm: "ScanVM", level: str) -> bool:
219
+ """Return ``True`` when any non-quantum-safe finding is at/above *level*."""
220
+ try:
221
+ threshold = _SEVERITY.index(level)
222
+ except ValueError:
223
+ return False
224
+ for finding in vm.findings:
225
+ rank = _SEVERITY.index(finding.risk_level) if finding.risk_level in _SEVERITY else None
226
+ if rank is not None and rank <= threshold:
227
+ return True
228
+ return False
@@ -0,0 +1,48 @@
1
+ """``ecdat version`` — version, engine, signature count, Python, and OS.
2
+
3
+ Also backs the global ``--version`` flag via
4
+ :func:`ecdat.cli.commands.version_cmd.render_version` in
5
+ :mod:`ecdat.cli.parser`. Everything heavy (the signature knowledge base) is
6
+ imported lazily inside :func:`render_version`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+
14
+ NAME = "version"
15
+
16
+
17
+ def register(subparsers: "argparse._SubParsersAction") -> None:
18
+ """Attach the ``version`` subcommand to *subparsers*."""
19
+ subparsers.add_parser(
20
+ "version",
21
+ help="Show version, engine, signature count, Python and OS",
22
+ description=(
23
+ "Print the ECDAT version line: distribution version, engine "
24
+ "package, number of loaded signatures, Python version, and OS."
25
+ ),
26
+ )
27
+
28
+
29
+ def render_version() -> str:
30
+ """Build the single-line version string (``ecdat <ver> \u00b7 ...``)."""
31
+ import platform
32
+
33
+ from ecdat import __version__
34
+ from ecdat_core.signature_loader import get_all_signatures
35
+
36
+ signature_count = len(get_all_signatures())
37
+ os_name = platform.system() or "unknown"
38
+ return (
39
+ f"ecdat {__version__} \u00b7 engine ecdat_core \u00b7 "
40
+ f"{signature_count} signatures \u00b7 Python {platform.python_version()} "
41
+ f"\u00b7 {os_name}"
42
+ )
43
+
44
+
45
+ def run(args) -> int:
46
+ """Write the version line to stdout; return exit code 0."""
47
+ sys.stdout.write(render_version() + "\n")
48
+ return 0
ecdat/cli/parser.py ADDED
@@ -0,0 +1,87 @@
1
+ """Argument parser construction for the ``ecdat`` CLI.
2
+
3
+ :func:`build_parser` assembles the global options and delegates subcommand
4
+ registration to each module in :mod:`ecdat.cli.commands`. Importing this
5
+ module must stay cheap: command modules defer heavy imports (Rich, the
6
+ scanner engine) to their ``run()`` functions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+
14
+ from ecdat.cli.commands import about as about_command
15
+ from ecdat.cli.commands import demo as demo_command
16
+ from ecdat.cli.commands import doctor as doctor_command
17
+ from ecdat.cli.commands import help_cmd as help_command
18
+ from ecdat.cli.commands import scan as scan_command
19
+ from ecdat.cli.commands import version_cmd as version_command
20
+
21
+ # Command name -> command module. Every module exposes ``register`` and
22
+ # ``run(args) -> int``; ``run`` is looked up at call time so tests can patch it.
23
+ COMMANDS: dict[str, object] = {
24
+ scan_command.NAME: scan_command,
25
+ demo_command.NAME: demo_command,
26
+ help_command.NAME: help_command,
27
+ doctor_command.NAME: doctor_command,
28
+ about_command.NAME: about_command,
29
+ version_command.NAME: version_command,
30
+ }
31
+
32
+
33
+ class _VersionAction(argparse.Action):
34
+ """Print the full version line to stdout and exit ``0``.
35
+
36
+ Deliberately writes to stdout (argparse's built-in version action uses
37
+ stderr) so ``ecdat --version`` and ``ecdat version`` emit identical,
38
+ pipe-friendly payloads. The version renderer is imported lazily so
39
+ importing this module never pulls in the signature knowledge base.
40
+ """
41
+
42
+ def __init__(self, option_strings, dest, **kwargs):
43
+ super().__init__(option_strings, dest, nargs=0, **kwargs)
44
+
45
+ def __call__(self, parser, namespace, values, option_string=None):
46
+ from ecdat.cli.commands.version_cmd import render_version
47
+
48
+ sys.stdout.write(render_version() + "\n")
49
+ parser.exit(0)
50
+
51
+
52
+ def build_parser() -> argparse.ArgumentParser:
53
+ """Build the top-level ``ecdat`` argument parser.
54
+
55
+ Returns:
56
+ An :class:`argparse.ArgumentParser` with the global flags
57
+ (``--version``, ``--no-color``, ``--debug``) and every registered
58
+ subcommand attached. The subcommand is optional — a bare ``ecdat``
59
+ invocation prints the banner and example commands.
60
+ """
61
+ parser = argparse.ArgumentParser(
62
+ prog="ecdat",
63
+ description="Cryptographic Discovery & Analysis Tool",
64
+ )
65
+
66
+ parser.add_argument(
67
+ "--version",
68
+ action=_VersionAction,
69
+ help="Show version info (engine, signatures, Python, OS) and exit",
70
+ )
71
+ parser.add_argument(
72
+ "--no-color",
73
+ action="store_true",
74
+ help="Disable coloured output",
75
+ )
76
+ parser.add_argument(
77
+ "--debug",
78
+ action="store_true",
79
+ help="Print tracebacks for unexpected errors",
80
+ )
81
+ parser.set_defaults(version=False, no_color=False, debug=False)
82
+
83
+ subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
84
+ for module in COMMANDS.values():
85
+ module.register(subparsers)
86
+
87
+ return parser
@@ -0,0 +1,75 @@
1
+ """Authentication service for the enterprise SSO gateway.
2
+
3
+ Handles user login, session token generation, and RSA-based request
4
+ signing for the internal API mesh. This module sits on the critical path
5
+ for every authenticated request — a compromise here cascades to the entire
6
+ platform.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import time
13
+ from typing import Optional
14
+
15
+ from Crypto.PublicKey import RSA
16
+ from Crypto.Signature import pkcs1_15
17
+ from Crypto.Hash import SHA256
18
+
19
+
20
+ # ── RSA key management ──────────────────────────────────────────────
21
+
22
+ # Legacy deployment: 1024-bit RSA key for internal signing.
23
+ # TODO: migrate to ML-DSA before Q4 security audit.
24
+ _SIGNING_KEY = RSA.generate(1024, b"legacy-seed-do-not-use-in-prod")
25
+
26
+
27
+ def load_signing_key() -> object:
28
+ """Load the RSA private key used for session token signing."""
29
+ return _SIGNING_KEY
30
+
31
+
32
+ def sign_request(payload: bytes) -> bytes:
33
+ """Sign an API request payload using RSA-PKCS#1 v1.5."""
34
+ key = load_signing_key()
35
+ h = SHA256.new(payload)
36
+ return pkcs1_15.new(key).sign(h)
37
+
38
+
39
+ def verify_request(payload: bytes, signature: bytes, pub_key: object) -> bool:
40
+ """Verify an API request signature against the sender's RSA public key."""
41
+ h = SHA256.new(payload)
42
+ try:
43
+ pkcs1_15.new(pub_key).verify(h, signature)
44
+ return True
45
+ except (ValueError, TypeError):
46
+ return False
47
+
48
+
49
+ # ── Session tokens ──────────────────────────────────────────────────
50
+
51
+ _TOKEN_EXPIRY_SECONDS = 3600
52
+
53
+
54
+ def create_session_token(user_id: str, roles: list[str]) -> dict:
55
+ """Create a signed session token with the given user context."""
56
+ now = int(time.time())
57
+ payload = {
58
+ "sub": user_id,
59
+ "roles": roles,
60
+ "iat": now,
61
+ "exp": now + _TOKEN_EXPIRY_SECONDS,
62
+ }
63
+ # Encode and sign
64
+ payload_bytes = str(payload).encode("utf-8")
65
+ signature = sign_request(payload_bytes)
66
+ return {"payload": payload_bytes.hex(), "sig": signature.hex()}
67
+
68
+
69
+ def _legacy_hash_password(password: str) -> str:
70
+ """MD5-based password hashing — kept for legacy credential migration.
71
+
72
+ DEPRECATED: This exists only so we can migrate old MD5-stored
73
+ credentials to bcrypt. Do NOT use for new accounts.
74
+ """
75
+ return hashlib.md5(password.encode("utf-8")).hexdigest()
@@ -0,0 +1,81 @@
1
+ // Package certs implements X.509 certificate chain verification and
2
+ // signing for the internal certificate authority.
3
+ //
4
+ // WARNING: This module uses SHA-1 for certificate fingerprinting, which
5
+ // is deprecated due to demonstrated collision attacks. Migrate to SHA-256.
6
+ package certs
7
+
8
+ import (
9
+ "crypto/sha1"
10
+ "crypto/sha256"
11
+ "crypto/x509"
12
+ "encoding/hex"
13
+ "fmt"
14
+ "io"
15
+ "os"
16
+ )
17
+
18
+ // FingerprintSHA1 returns the SHA-1 fingerprint of a DER-encoded
19
+ // certificate. Used by legacy monitoring dashboards.
20
+ //
21
+ // Deprecated: Use FingerprintSHA256 for all new fingerprinting.
22
+ func FingerprintSHA1(derBytes []byte) string {
23
+ h := sha1.Sum(derBytes)
24
+ return hex.EncodeToString(h[:])
25
+ }
26
+
27
+ // FingerprintSHA256 returns the SHA-256 fingerprint of a DER-encoded
28
+ // certificate. This is the recommended replacement for FingerprintSHA1.
29
+ func FingerprintSHA256(derBytes []byte) string {
30
+ h := sha256.Sum256(derBytes)
31
+ return hex.EncodeToString(h[:])
32
+ }
33
+
34
+ // VerifyCertChain verifies that the leaf certificate is signed by one
35
+ // of the trusted roots in the system trust store.
36
+ func VerifyCertChain(leaf *x509.Certificate) error {
37
+ pool, err := x509.SystemCertPool()
38
+ if err != nil {
39
+ return fmt.Errorf("failed to load system cert pool: %w", err)
40
+ }
41
+ opts := x509.VerifyOptions{
42
+ Roots: pool,
43
+ }
44
+ _, err = leaf.Verify(opts)
45
+ return err
46
+ }
47
+
48
+ // CertFileSHA1Fingerprint reads a PEM/DER certificate file and returns
49
+ // its SHA-1 fingerprint. Kept for backward compat with legacy dashboards.
50
+ //
51
+ // Deprecated: Use CertFileSHA256Fingerprint instead.
52
+ func CertFileSHA1Fingerprint(path string) (string, error) {
53
+ data, err := os.ReadFile(path)
54
+ if err != nil {
55
+ return "", fmt.Errorf("read cert file: %w", err)
56
+ }
57
+ h := sha1.Sum(data)
58
+ return hex.EncodeToString(h[:]), nil
59
+ }
60
+
61
+ // CertFileSHA256Fingerprint reads a certificate file and returns
62
+ // its SHA-256 fingerprint.
63
+ func CertFileSHA256Fingerprint(path string) (string, error) {
64
+ data, err := os.ReadFile(path)
65
+ if err != nil {
66
+ return "", fmt.Errorf("read cert file: %w", err)
67
+ }
68
+ h := sha256.Sum256(data)
69
+ return hex.EncodeToString(h[:]), nil
70
+ }
71
+
72
+ // LegacyCAFingerprint returns the SHA-1 fingerprint of a certificate
73
+ // read from a reader. Used by the CA migration tool.
74
+ func LegacyCAFingerprint(r io.Reader) (string, error) {
75
+ data, err := io.ReadAll(r)
76
+ if err != nil {
77
+ return "", err
78
+ }
79
+ h := sha1.Sum(data)
80
+ return hex.EncodeToString(h[:]), nil
81
+ }
@@ -0,0 +1,48 @@
1
+ // Package keyexchange implements Diffie-Hellman key agreement for the
2
+ // secure inter-service communication channel.
3
+ //
4
+ // WARNING: Classic DH with 2048-bit prime is quantum-vulnerable.
5
+ // This module is scheduled for ML-KEM migration before production launch.
6
+ package keyexchange
7
+
8
+ import (
9
+ "crypto/dh"
10
+ "fmt"
11
+ "math/big"
12
+ )
13
+
14
+ // Group14 is the 2048-bit MODP group from RFC 3526 (Group 14).
15
+ var Group14 *dh.Group
16
+
17
+ // DHParams holds the parameters for a Diffie-Hellman key exchange.
18
+ type DHParams struct {
19
+ Prime *big.Int
20
+ Generator int64
21
+ }
22
+
23
+ // GenerateKeyPair creates a new DH key pair using Group 14.
24
+ // Returns (privateKey, publicKey) as byte slices.
25
+ func GenerateKeyPair() ([]byte, []byte, error) {
26
+ privKey, err := dh.GenerateKey(Group14)
27
+ if err != nil {
28
+ return nil, nil, fmt.Errorf("DH key generation failed: %w", err)
29
+ }
30
+ pubKey := Group14.PublicKey(privKey)
31
+ return privKey, pubKey.Bytes(), nil
32
+ }
33
+
34
+ // ComputeSharedSecret derives the shared secret from a private key
35
+ // and the peer's public key.
36
+ func ComputeSharedSecret(privKey *dh.PrivateKey, peerPubKey *dh.PublicKey) []byte {
37
+ return privKey.PublicKey().SharedKey(peerPubKey)
38
+ }
39
+
40
+ // GenerateDHKey is a convenience wrapper that generates and returns
41
+ // just the private key bytes (useful for single-key contexts).
42
+ func GenerateDHKey() ([]byte, error) {
43
+ privKey, err := dh.GenerateKey(Group14)
44
+ if err != nil {
45
+ return nil, err
46
+ }
47
+ return privKey.PublicKey().Bytes(), nil
48
+ }
@@ -0,0 +1,78 @@
1
+ package com.showcase.legacy;
2
+
3
+ import javax.crypto.Cipher;
4
+ import javax.crypto.KeyGenerator;
5
+ import javax.crypto.SecretKey;
6
+ import javax.crypto.spec.DESedeKeySpec;
7
+ import java.security.Key;
8
+ import java.security.MessageDigest;
9
+ import java.util.Base64;
10
+
11
+ /**
12
+ * Legacy cryptographic utilities — retained for backward compatibility
13
+ * with old client SDKs that still use 3DES and MD5-based checksums.
14
+ *
15
+ * Both algorithms are broken or deprecated and must be replaced:
16
+ * - 3DES: 112-bit effective key, slow, NIST retired in 2023.
17
+ * - MD5: collision attacks demonstrated, unsuitable for security use.
18
+ */
19
+ public final class LegacyCrypto {
20
+
21
+ private static final String DES_EDE_CIPHER = "DESede/ECB/PKCS5Padding";
22
+
23
+ private LegacyCrypto() {}
24
+
25
+ /**
26
+ * Encrypt data with Triple DES (3DES).
27
+ *
28
+ * @param plaintext data to encrypt
29
+ * @param keyBytes 24-byte (192-bit) 3DES key
30
+ * @return Base64-encoded ciphertext
31
+ */
32
+ public static String encrypt3DES(String plaintext, byte[] keyBytes)
33
+ throws Exception {
34
+ DESedeKeySpec spec = new DESedeKeySpec(keyBytes);
35
+ Key key = javax.crypto.SecretKeyFactory.getInstance("DESede")
36
+ .generateSecret(spec);
37
+
38
+ Cipher cipher = Cipher.getInstance(DES_EDE_CIPHER);
39
+ cipher.init(Cipher.ENCRYPT_MODE, key);
40
+ byte[] ct = cipher.doFinal(plaintext.getBytes("UTF-8"));
41
+ return Base64.getEncoder().encodeToString(ct);
42
+ }
43
+
44
+ /**
45
+ * Generate a random 3DES key.
46
+ */
47
+ public static SecretKey generate3DESKey() throws Exception {
48
+ KeyGenerator kg = KeyGenerator.getInstance("DESede");
49
+ kg.init(168); // 168-bit key, 112-bit effective security
50
+ return kg.generateKey();
51
+ }
52
+
53
+ /**
54
+ * Compute an MD5 digest of the input bytes.
55
+ *
56
+ * @deprecated Use SHA-256 instead. MD5 has broken collision resistance.
57
+ */
58
+ @Deprecated
59
+ public static String md5Checksum(byte[] data) throws Exception {
60
+ MessageDigest md = MessageDigest.getInstance("MD5");
61
+ byte[] digest = md.digest(data);
62
+ StringBuilder sb = new StringBuilder();
63
+ for (byte b : digest) {
64
+ sb.append(String.format("%02x", b));
65
+ }
66
+ return sb.toString();
67
+ }
68
+
69
+ /**
70
+ * Compute an MD5 hash of a string (used by legacy credential store).
71
+ */
72
+ @Deprecated
73
+ public static String legacyCredentialHash(String credential) throws Exception {
74
+ MessageDigest md = MessageDigest.getInstance("MD5");
75
+ byte[] raw = md.digest(credential.getBytes("UTF-8"));
76
+ return Base64.getEncoder().encodeToString(raw);
77
+ }
78
+ }