Skip to content

Commit 158bb6b

Browse files
h0tak88rclaude
andcommitted
fix(ywh): accept otpauth:// URI as 2FA secret; quiet ipatool entrypoint; ulimit before useradd
- YWH 2FA: users paste the whole otpauth:// URI (or a secret=… query) from the QR code instead of the bare base32 seed, so TOTP generation failed with 'Non-base32 digit found' and the account stayed INVALID. extractTOTPSecret now pulls the secret= value out of a URI/query before decoding. Verified live: the stored 0x88 URI now generates a valid code and /account/totp returns a JWT. Existing accounts work without re-entry. UI label + test updated. - entrypoint: stop printing 'IPATOOL_KEYCHAIN_PASSPHRASE is NOT set - iOS downloads will fail!' as an error when ipatool is simply unused (optional). One quiet line when unconfigured; only warns if partially configured. - Dockerfile: 'ulimit -n 1024' before useradd so its file-descriptor close-loop can't busy-spin for minutes when the build container's nofile limit is huge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ad60a9b commit 158bb6b

5 files changed

Lines changed: 56 additions & 24 deletions

File tree

Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ RUN mkdir -p /app/new-results /app/nuclei_templates || true
9797
RUN chmod +x /usr/local/bin/autoar-entrypoint \
9898
&& echo "All modules are now Go-based - pure Go implementation" || true
9999

100-
# Add a non-root user
101-
RUN useradd -m -u 10001 autoar && \
100+
# Add a non-root user.
101+
# `ulimit -n 1024` first: some libc/useradd versions loop over every possible file
102+
# descriptor up to the (build container's) nofile limit, which can busy-spin for
103+
# minutes when that limit is huge. Capping it keeps this step fast.
104+
RUN ulimit -n 1024 2>/dev/null || true; \
105+
useradd -m -u 10001 autoar && \
102106
chown -R autoar:autoar /app && \
103107
chown autoar:autoar /usr/local/bin/autoar-entrypoint
104108
USER autoar

internal/api/programs_ywh_auth.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io"
1111
"net/http"
12+
"net/url"
1213
"strings"
1314
"sync"
1415
"time"
@@ -20,8 +21,26 @@ import (
2021
// setting up 2FA, NOT a one-time code.
2122
func generateTOTP(secret string) (string, error) { return totpAt(secret, time.Now()) }
2223

24+
// extractTOTPSecret returns the bare base32 seed from whatever the user pasted.
25+
// Users often paste the whole otpauth:// URI (or a "…secret=XXXX&…" query) from
26+
// the 2FA QR code instead of just the seed — pull the secret= value out of it.
27+
func extractTOTPSecret(raw string) string {
28+
s := strings.TrimSpace(raw)
29+
if i := strings.Index(strings.ToLower(s), "secret="); i >= 0 {
30+
v := s[i+len("secret="):]
31+
if j := strings.IndexAny(v, "&?#\r\n\t "); j >= 0 {
32+
v = v[:j]
33+
}
34+
if dec, err := url.QueryUnescape(v); err == nil {
35+
v = dec
36+
}
37+
return v
38+
}
39+
return s
40+
}
41+
2342
func totpAt(secret string, t time.Time) (string, error) {
24-
s := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(secret), " ", ""))
43+
s := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(extractTOTPSecret(secret)), " ", ""))
2544
if s == "" {
2645
return "", fmt.Errorf("empty TOTP secret")
2746
}

internal/api/programs_ywh_auth_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,22 @@ func TestTOTPRFC6238(t *testing.T) {
3333
t.Errorf("spaced/lowercase secret should parse: %v", err)
3434
}
3535
}
36+
37+
// TestExtractTOTPSecret covers the common mistake of pasting the whole otpauth://
38+
// URI (or a secret= query) instead of just the base32 seed.
39+
func TestExtractTOTPSecret(t *testing.T) {
40+
cases := map[string]string{
41+
"GEZDGNBVGY3TQOJQ": "GEZDGNBVGY3TQOJQ", // bare seed → unchanged
42+
"otpauth://totp/YesWeHack:me@x.com?secret=GEZDGNBVGY3TQOJQ&issuer=YesWeHack&digits=6": "GEZDGNBVGY3TQOJQ",
43+
"secret=GEZDGNBVGY3TQOJQ&period=30": "GEZDGNBVGY3TQOJQ",
44+
}
45+
for in, want := range cases {
46+
if got := extractTOTPSecret(in); got != want {
47+
t.Errorf("extractTOTPSecret(%q) = %q, want %q", in, got, want)
48+
}
49+
}
50+
// An otpauth URI must now produce a valid code (RFC vector secret embedded).
51+
if code, err := totpAt("otpauth://totp/x?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", time.Unix(59, 0)); err != nil || code != "287082" {
52+
t.Errorf("otpauth URI TOTP = %q, err=%v; want 287082", code, err)
53+
}
54+
}

internal/api/ui/pages/settings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@
290290
// Friendly placeholders for account fields (esp. the YWH 2FA seed).
291291
const ACCT_FIELD_LABELS = {
292292
username: 'Username', token: 'Token', email: 'Email', password: 'Password',
293-
totp_secret: '2FA secret (base32) — optional',
293+
totp_secret: '2FA secret or otpauth:// URI — optional',
294294
};
295295

296296
async function loadSettingsAccounts() {

internal/scanner/entrypoint/entrypoint.go

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,16 @@ func main() {
2424
os.Setenv("AUTOAR_ENV", "docker")
2525
fmt.Println("[entrypoint] Configuration loaded successfully")
2626

27-
fmt.Println("[entrypoint] Checking IPATOOL environment variables...")
28-
if val := os.Getenv("IPATOOL_EMAIL"); val != "" {
29-
fmt.Printf("[entrypoint] IPATOOL_EMAIL is set (length: %d)\n", len(val))
30-
} else {
31-
fmt.Println("[entrypoint] IPATOOL_EMAIL is NOT set")
32-
}
33-
if val := os.Getenv("IPATOOL_PASSWORD"); val != "" {
34-
fmt.Printf("[entrypoint] IPATOOL_PASSWORD is set (length: %d)\n", len(val))
35-
} else {
36-
fmt.Println("[entrypoint] IPATOOL_PASSWORD is NOT set")
37-
}
38-
if val := os.Getenv("IPATOOL_KEYCHAIN_PASSPHRASE"); val != "" {
39-
fmt.Printf("[entrypoint] IPATOOL_KEYCHAIN_PASSPHRASE is set (length: %d)\n", len(val))
40-
} else {
41-
fmt.Println("[entrypoint] IPATOOL_KEYCHAIN_PASSPHRASE is NOT set - iOS downloads will fail!")
42-
}
43-
if val := os.Getenv("IPATOOL_AUTH_CODE"); val != "" {
44-
fmt.Printf("[entrypoint] IPATOOL_AUTH_CODE is set (length: %d)\n", len(val))
45-
} else {
46-
fmt.Println("[entrypoint] IPATOOL_AUTH_CODE is not set (optional)")
27+
// ipatool (iOS .ipa downloads) is OPTIONAL — only relevant if the operator
28+
// configured IPATOOL_* credentials. Don't emit a scary "iOS downloads will
29+
// fail" error line when the feature is simply unused (the common case).
30+
switch {
31+
case os.Getenv("IPATOOL_EMAIL") == "" && os.Getenv("IPATOOL_KEYCHAIN_PASSPHRASE") == "":
32+
fmt.Println("[entrypoint] ipatool (iOS downloads) not configured — optional, skipping")
33+
case os.Getenv("IPATOOL_EMAIL") != "" && os.Getenv("IPATOOL_PASSWORD") != "" && os.Getenv("IPATOOL_KEYCHAIN_PASSPHRASE") != "":
34+
fmt.Println("[entrypoint] ipatool (iOS downloads) configured")
35+
default:
36+
fmt.Println("[entrypoint] ipatool partially configured — set IPATOOL_EMAIL, IPATOOL_PASSWORD and IPATOOL_KEYCHAIN_PASSPHRASE to enable iOS downloads")
4737
}
4838

4939
fmt.Println("[entrypoint] Database schema initialization delegated to API/Bot startup")

0 commit comments

Comments
 (0)